using ytLive.Services;

namespace ytLive.Services.Audio;

/// <summary>
/// Owns the mic + desktop/game capture sources (TASK 4 ship step 4) and drives
/// the footer meters; since TASK 9 it is also the live audio path into the
/// encoder. Capture runs for the app's lifetime so both bars stay live in
/// preview: mic samples are downmixed to mono, resampled to 48 kHz, put
/// through the voice chain (bass → treble → noise gate → compressor), THEN
/// level-metered and queued; loopback samples are resampled to 48 kHz stereo
/// and queued. While live (<see cref="StartLive"/>), a 10 ms loop drains both
/// queues, applies the honest gains (mic = MicVolume, loopback = GameAudioVolume
/// × auto-duck when the mic is hot), mixes them to stereo float and writes the
/// chunk to the encoder's audio pipe.
/// </summary>
public sealed class AudioMixer : IDisposable
{
    public const int OutputSampleRate = 48000;

    private static readonly TimeSpan DefaultMixInterval = TimeSpan.FromMilliseconds(10);

    private readonly IAudioSource _mic;
    private readonly IAudioSource _loopback;
    private readonly AudioLevelMeter _meter;
    private readonly AudioLevelMeter _loopbackMeter;
    private readonly Action<string>? _log;
    private readonly Func<double>? _micGain;
    private readonly Func<double>? _loopbackGain;
    private readonly TimeSpan _mixInterval;
    private bool _started;

    // Live path (TASK 9): per-source resampling → voice chain on the mic →
    // thread-safe queues → the ducker + gain/mix loop → the encoder's pipe.
    private readonly AudioRingBuffer _micBuffer;
    private readonly AudioRingBuffer _loopbackBuffer;
    private readonly VoiceFilterChain _voiceChain;
    private readonly AutoDucker _ducker;
    private TinyResampler? _micResampler;
    private TinyResampler? _loopbackResampler;
    private CancellationTokenSource? _liveCts;
    private NamedPipeAudioWriter _pipe = new();
    private float[]? _micChunk;
    private float[]? _loopbackChunk;
    private float[]? _mixBuffer;

    public AudioMixer(
        IAudioSource mic,
        IAudioSource loopback,
        Action<string>? log = null,
        Func<double>? micGain = null,
        Func<double>? loopbackGain = null,
        TimeSpan? mixInterval = null)
    {
        _mic = mic;
        _loopback = loopback;
        _meter = new AudioLevelMeter();
        _loopbackMeter = new AudioLevelMeter();
        _log = log;
        _micGain = micGain;
        _loopbackGain = loopbackGain;
        _mixInterval = mixInterval ?? DefaultMixInterval;

        var seconds = _mixInterval.TotalSeconds;
        var framesPerTick = Math.Max(1, (int)Math.Round(OutputSampleRate * seconds));
        _micBuffer = new AudioRingBuffer(OutputSampleRate * 2);
        _loopbackBuffer = new AudioRingBuffer(OutputSampleRate * 2 * 2);
        _voiceChain = new VoiceFilterChain(OutputSampleRate);
        _ducker = new AutoDucker();
        _micChunk = new float[framesPerTick];
        _loopbackChunk = new float[framesPerTick * 2];
        _mixBuffer = new float[framesPerTick * 2];

        _mic.Started += OnMicStarted;
        _mic.SampleReady += OnMicSample;
        _loopback.SampleReady += OnLoopbackSample;
        _mic.Failed += OnMicFailed;
        _loopback.Failed += OnLoopbackFailed;
    }

    /// <summary>Current smoothed mic level (0..1), post voice chain.</summary>
    public float MicLevel => _meter.Level;

    /// <summary>Current smoothed desktop/game level (0..1).</summary>
    public float LoopbackLevel => _loopbackMeter.Level;

    /// <summary>Raised whenever the smoothed mic level changes.</summary>
    public event Action<float>? MicLevelChanged;

    /// <summary>Raised whenever the smoothed desktop/game level changes.</summary>
    public event Action<float>? LoopbackLevelChanged;

    /// <summary>Raised when the mic capture comes up (the status dot goes green).</summary>
    public event Action? MicConnected;

    /// <summary>Raised when the mic capture fails or dies (the status dot goes yellow).</summary>
    public event Action<Exception>? MicFailed;

    public void Start()
    {
        if (_started)
            return;

        _started = true;
        _meter.Reset();
        _loopback.Start();
        _mic.Start();
    }

    /// <summary>Swaps the mic source without touching loopback — used when the
    /// creator picks a different device mid-session. The level resets and the
    /// new source raises <see cref="MicConnected"/> or <see cref="MicFailed"/>.</summary>
    public void RestartMic()
    {
        _mic.Stop();
        _meter.Reset();
        _micBuffer.Clear();
        _micResampler?.Reset();
        _voiceChain.Reset();
        MicLevelChanged?.Invoke(0);
        _mic.Start();
    }

    public void Stop()
    {
        if (!_started)
            return;

        _started = false;
        StopLive();
        _mic.Stop();
        _loopback.Stop();
        _meter.Reset();
        _loopbackMeter.Reset();
        MicLevelChanged?.Invoke(0);
        LoopbackLevelChanged?.Invoke(0);
    }

    /// <summary>Begins the live mix loop: drains the capture queues, applies the
    /// honest gains + auto-duck, and streams stereo float into the named audio
    /// pipe. Idempotent — safe to call once per go-live.</summary>
    public void StartLive(string pipeName)
    {
        if (_liveCts != null)
            return;

        var cts = new CancellationTokenSource();
        _liveCts = cts;
        _pipe = new NamedPipeAudioWriter();
        _pipe.Start(pipeName);
        _ = Task.Run(() => LiveLoopAsync(_pipe, cts.Token));
    }

    /// <summary>Ends the live mix loop and closes the audio pipe — ffmpeg sees
    /// EOF on the audio input. Safe when not live.</summary>
    public void StopLive()
    {
        _liveCts?.Cancel();
        _liveCts = null;
        _pipe.Stop();
    }

    public void Dispose()
    {
        Stop();
        _mic.Started -= OnMicStarted;
        _mic.SampleReady -= OnMicSample;
        _loopback.SampleReady -= OnLoopbackSample;
        _mic.Failed -= OnMicFailed;
        _loopback.Failed -= OnLoopbackFailed;
        _mic.Dispose();
        _loopback.Dispose();
    }

    private void OnMicStarted()
    {
        MicConnected?.Invoke();
    }

    private void OnMicSample(AudioSample sample)
    {
        var mono = DownmixToMono(sample);
        mono = ResampleMic(mono, sample.SampleRate);
        for (var i = 0; i < mono.Length; i++)
            mono[i] = _voiceChain.Process(mono[i]);
        _micBuffer.Write(mono);

        // Push unconditionally: the ?. on the event would otherwise skip the
        // argument (and the meter update) when nothing is subscribed yet. The
        // level is the POST-filtered signal — the meter shows what the stream
        // will carry.
        var level = _meter.Push(new AudioSample(mono, OutputSampleRate, 1));
        MicLevelChanged?.Invoke(level);
    }

    private void OnLoopbackSample(AudioSample sample)
    {
        var data = ResampleLoopback(sample.Samples, sample.SampleRate);
        _loopbackBuffer.Write(data);

        var level = _loopbackMeter.Push(new AudioSample(data, OutputSampleRate, sample.Channels));
        LoopbackLevelChanged?.Invoke(level);
    }

    private void OnMicFailed(Exception ex)
    {
        _log?.Invoke($"Mic capture failed: {ex.Message}");
        MicLevelChanged?.Invoke(0);
        MicFailed?.Invoke(ex);
    }

    private void OnLoopbackFailed(Exception ex)
    {
        _log?.Invoke($"Desktop audio capture failed: {ex.Message}");
    }

    private static float[] DownmixToMono(AudioSample sample)
    {
        if (sample.Channels <= 1)
            return sample.Samples;

        var frames = sample.Samples.Length / sample.Channels;
        var mono = new float[frames];
        for (var i = 0; i < frames; i++)
        {
            var sum = 0f;
            for (var c = 0; c < sample.Channels; c++)
                sum += sample.Samples[i * sample.Channels + c];
            mono[i] = sum / sample.Channels;
        }
        return mono;
    }

    private float[] ResampleMic(float[] mono, int inputRate)
    {
        if (inputRate == OutputSampleRate)
            return mono;
        _micResampler ??= new TinyResampler(inputRate, OutputSampleRate);
        if (_micResampler.NeedsResampling)
            return _micResampler.Process(mono);
        return mono;
    }

    private float[] ResampleLoopback(float[] interleaved, int inputRate)
    {
        if (inputRate == OutputSampleRate)
            return interleaved;
        _loopbackResampler ??= new TinyResampler(inputRate, OutputSampleRate);
        if (_loopbackResampler.NeedsResampling)
            return _loopbackResampler.Process(interleaved);
        return interleaved;
    }

    private async Task LiveLoopAsync(IAudioPipeWriter pipe, CancellationToken cancellationToken)
    {
        var mixBuffer = _mixBuffer!;
        var micChunk = _micChunk!;
        var loopbackChunk = _loopbackChunk!;
        while (!cancellationToken.IsCancellationRequested)
        {
            var nextTick = DateTime.UtcNow + _mixInterval;
            try
            {
                FillAndMix(micChunk, loopbackChunk, mixBuffer);
                await pipe.WriteAsync(mixBuffer, cancellationToken).ConfigureAwait(false);
            }
            catch (OperationCanceledException)
            {
                break;
            }
            catch (Exception ex)
            {
                _log?.Invoke($"Audio live loop error: {ex.Message}");
            }

            var delay = nextTick - DateTime.UtcNow;
            if (delay > TimeSpan.Zero)
            {
                try
                {
                    await Task.Delay(delay, cancellationToken).ConfigureAwait(false);
                }
                catch (OperationCanceledException)
                {
                    break;
                }
            }
        }
    }

    /// <summary>Drains one tick's worth of mic + loopback, silence-fills any
    /// underrun, applies the ducker and the honest gains, and mixes to stereo.</summary>
    private float FillAndMix(float[] micChunk, float[] loopbackChunk, float[] mix)
    {
        var micCount = _micBuffer.Read(micChunk, micChunk.Length);
        var loopCount = _loopbackBuffer.Read(loopbackChunk, loopbackChunk.Length);

        float micRms = 0;
        if (micCount == 0)
        {
            Array.Clear(micChunk, 0, micChunk.Length);
        }
        else
        {
            double sumSquares = 0;
            for (var i = 0; i < micCount; i++)
                sumSquares += micChunk[i] * micChunk[i];
            micRms = (float)Math.Sqrt(sumSquares / micCount);
            if (micCount < micChunk.Length)
                Array.Clear(micChunk, micCount, micChunk.Length - micCount);
        }

        if (loopCount == 0)
        {
            Array.Clear(loopbackChunk, 0, loopbackChunk.Length);
        }
        else if (loopCount < loopbackChunk.Length)
        {
            Array.Clear(loopbackChunk, loopCount, loopbackChunk.Length - loopCount);
        }

        var duck = _ducker.Update(micRms);
        var micGain = (float)(_micGain?.Invoke() ?? 1.0);
        var loopGain = (float)(_loopbackGain?.Invoke() ?? 1.0) * duck;

        var frames = micChunk.Length;
        for (var i = 0; i < frames; i++)
        {
            var m = micChunk[i] * micGain;
            mix[i * 2] = m + loopbackChunk[i * 2] * loopGain;
            mix[i * 2 + 1] = m + loopbackChunk[i * 2 + 1] * loopGain;
        }
        return micRms;
    }
}
