using System.Diagnostics;
using ytLive.Helpers;
using ytLive.Models;

namespace ytLive.Services.Encoder;

/// <summary>
/// The default <see cref="IFfmpegEncoder"/>: spawns <c>ffmpeg.exe</c> (resolved via
/// <see cref="IFfmpegLocator"/>), feeds raw BGRA frames into stdin, and parses the
/// <c>-stats</c> progress lines into <see cref="StreamHealth"/>. Encoder choice is
/// probed from the binary's <c>-encoders</c> listing (hardware NVENC/QSV/AMF first,
/// OpenH264 software fallback — never libx264, see the license posture) unless
/// <see cref="EncoderOptions.VideoEncoder"/> forces one.
///
/// Graceful stop = close stdin (EOF) → ffmpeg finalizes the FLV and exits by itself;
/// a watchdogs kill fires only if it hasn't exited shortly after EOF.
/// </summary>
public sealed class FfmpegEncoder : IFfmpegEncoder
{
    public event EventHandler<StreamHealth>? HealthUpdated;
    public event EventHandler<string>? ProcessFailed;

    private readonly IFfmpegLocator _locator;
    private readonly Func<IEncoderProcess> _processFactory;

    private readonly object _gate = new();
    private IEncoderProcess? _process;
    private EncoderOptions? _options;
    private StreamHealth _health = new() { Status = StreamStatus.Offline };
    private Task? _stderrLoop;
    private bool _stopRequested;

    public FfmpegEncoder(
        IFfmpegLocator locator,
        Func<IEncoderProcess>? processFactory = null)
    {
        _locator = locator;
        _processFactory = processFactory ?? (() => new FfmpegEncoderProcess());
    }

    public bool IsRunning { get; private set; }

    public async Task StartAsync(EncoderOptions options, CancellationToken cancellationToken = default)
    {
        if (options == null) throw new ArgumentNullException(nameof(options));
        if (string.IsNullOrWhiteSpace(options.RtmpUrl))
            throw new ArgumentException("An RTMP ingestion URL is required.", nameof(options));

        lock (_gate)
        {
            if (IsRunning) throw new InvalidOperationException("The encoder is already running.");
            _options = options;
        }

        var ffmpegPath = await _locator.LocateAsync(cancellationToken).ConfigureAwait(false);
        var encoder = options.VideoEncoder ?? await ProbeEncoderAsync(ffmpegPath, cancellationToken).ConfigureAwait(false);
        var args = FfmpegArgs.Build(options, encoder);

        var startInfo = new ProcessStartInfo
        {
            FileName = ffmpegPath,
            UseShellExecute = false,
            RedirectStandardInput = true,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            CreateNoWindow = true,
        };
        foreach (var arg in args) startInfo.ArgumentList.Add(arg);

        IEncoderProcess process;
        try
        {
            process = _processFactory();
            process.Start(startInfo);
        }
        catch (Exception ex)
        {
            AppLog.Write(ex, "FFmpeg encoder: failed to start subprocess");
            lock (_gate) _options = null;
            throw;
        }

        lock (_gate)
        {
            _process = process;
            IsRunning = true;
            _health = new StreamHealth { Status = StreamStatus.Streaming };
        }

        _stopRequested = false;
        _stderrLoop = RunStderrLoopAsync(process);
    }

    /// <summary>
    /// Write one raw BGRA frame to ffmpeg's stdin. Serialized internally; callers
    /// (the compositor pump) may race freely. Frames are written as-is — the caller
    /// paces to capture rate (the compositor's job, ship step 5).
    /// </summary>
    public async Task SubmitFrameAsync(VideoFrame frame, CancellationToken cancellationToken = default)
    {
        if (frame == null) throw new ArgumentNullException(nameof(frame));
        IEncoderProcess? process;
        lock (_gate)
        {
            if (!IsRunning) throw new InvalidOperationException("The encoder is not running.");
            process = _process;
        }

        var bytes = frame.BgraPixels;
        await process!.StandardInput.WriteAsync(bytes, cancellationToken).ConfigureAwait(false);
        await process.StandardInput.FlushAsync(cancellationToken).ConfigureAwait(false);
    }

    public async Task StopAsync(CancellationToken cancellationToken = default)
    {
        IEncoderProcess? process;
        Task? loop;
        lock (_gate)
        {
            if (!IsRunning) return;
            _stopRequested = true;
            process = _process;
            loop = _stderrLoop;
        }

        try
        {
            process!.StandardInput.Dispose(); // EOF → ffmpeg finalizes + exits
        }
        catch (Exception ex)
        {
            AppLog.Write(ex, "FFmpeg encoder: closing stdin failed");
        }

        try
        {
            using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            timeout.CancelAfter(TimeSpan.FromSeconds(10));
            await process!.WaitForExitAsync(timeout.Token).ConfigureAwait(false);
        }
        catch (OperationCanceledException)
        {
            AppLog.Write("FFmpeg encoder: did not exit after stdin EOF — killing");
            process!.Kill();
        }
        catch (Exception ex)
        {
            AppLog.Write(ex, "FFmpeg encoder: waiting for exit failed");
            process!.Kill();
        }

        try
        {
            if (loop != null) await loop.ConfigureAwait(false);
        }
        catch (Exception ex)
        {
            AppLog.Write(ex, "FFmpeg encoder: stderr loop faulted during stop");
        }

        process.Dispose();

        lock (_gate)
        {
            IsRunning = false;
            _health.Status = StreamStatus.Offline;
            _health.LastError = null;
            _process = null;
            _options = null;
        }

        AppLog.Write($"FFmpeg encoder stopped (exit {process.ExitCode})");
    }

    public void Dispose()
    {
        lock (_gate)
        {
            if (!IsRunning) return;
            _process?.Kill();
            _process?.Dispose();
            _process = null;
            IsRunning = false;
        }
    }

    private async Task<string> ProbeEncoderAsync(string ffmpegPath, CancellationToken cancellationToken)
    {
        try
        {
            var startInfo = new ProcessStartInfo
            {
                FileName = ffmpegPath,
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                CreateNoWindow = true,
            };
            startInfo.ArgumentList.Add("-hide_banner");
            startInfo.ArgumentList.Add("-encoders");

            using var probe = _processFactory();
            probe.Start(startInfo);
            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            cts.CancelAfter(TimeSpan.FromSeconds(15));
            var output = await probe.StandardOutput.ReadToEndAsync(cts.Token).ConfigureAwait(false);
            return FfmpegEncoderPicker.Pick(output);
        }
        catch (Exception ex)
        {
            AppLog.Write(ex, "FFmpeg encoder: encoder probe failed — falling back to software");
            return FfmpegEncoderPicker.Preference[^1];
        }
    }

    private Task RunStderrLoopAsync(IEncoderProcess process)
    {
        return Task.Run(async () =>
        {
            try
            {
                while (true)
                {
                    var line = await process.StandardError.ReadLineAsync().ConfigureAwait(false);
                    if (line == null) break;
                    OnStderrLine(process, line);
                }

                var code = process.ExitCode;
                var stillRunning = false;
                lock (_gate) stillRunning = IsRunning;
                if (stillRunning && !_stopRequested && code != 0)
                {
                    AppLog.Write($"FFmpeg encoder: subprocess exited unexpectedly ({code})");
                    _health.LastError = $"FFmpeg exited with code {code}";
                    ProcessFailed?.Invoke(this, $"FFmpeg exited with code {code}");
                }
            }
            catch (Exception ex)
            {
                AppLog.Write(ex, "FFmpeg encoder: stderr loop faulted");
            }
        });
    }

    private void OnStderrLine(IEncoderProcess process, string line)
    {
        var progress = FfmpegProgressParser.TryParse(line);
        if (progress == null) return;

        var dropped = Math.Max(0, (long)Math.Round(progress.Value.Fps * progress.Value.Duration.TotalSeconds) - progress.Value.Frame);

        lock (_gate)
        {
            _health.CurrentBitrate = progress.Value.BitrateKbps;
            _health.FPS = progress.Value.Fps;
            _health.DroppedFrames = (int)dropped;
            _health.StreamDuration = progress.Value.Duration;
        }

        HealthUpdated?.Invoke(this, _health);
    }
}
