using System.Reactive.Linq;
using System.Runtime.Versioning;
using Microsoft.Extensions.Logging;
using DragonISO.Engine.Controller;
using DragonISO.Engine.Domain;
using DragonISO.Engine.Interop;
using DragonISO.Engine.Logging;
using DragonISO.Engine.NdiInterop;
using DragonISO.Engine.Persistence;
using DragonISO.Engine.Pipeline;

namespace DragonISO.Console;

using SysConsole = System.Console;

/// <summary>
/// Headless smoke runner. Wires up the engine end-to-end against the production NDI P/Invoke
/// interop and prints participant updates, alerts, and ISO state. Useful for first-validation
/// against a real Teams meeting before the WPF UI is alive.
///
/// Requires Windows + the NDI Runtime installed.
///
/// Usage:
///   Dragon-ISO-console                   # discover only â€” print participants as they appear/leave
///   Dragon-ISO-console --enable-all      # auto-enable an ISO for every discovered participant
///   Dragon-ISO-console --list-sources    # diagnostic: print every raw NDI source string visible
///                                        on the network for ~5s, then exit. Useful for debugging
///                                        why expected Teams sources aren't being classified.
///   Dragon-ISO-console --test-pattern    # broadcast a synthetic NDI source named Dragon-ISO_TEST
///                                        with SMPTE color bars + sweep band. Useful for
///                                        verifying NDI runtime + discovery without Teams running,
///                                        and for demoing the product. Runs until Ctrl+C.
///   Dragon-ISO-console --version         # print engine version, NDI runtime version, exit codes,
///                                        then exit 0. Useful for support requests.
/// </summary>
public static class Program
{
    public static async Task<int> Main(string[] args)
    {
        var enableAll = args.Contains("--enable-all", StringComparer.OrdinalIgnoreCase);
        var listSources = args.Contains("--list-sources", StringComparer.OrdinalIgnoreCase);
        var testPattern = args.Contains("--test-pattern", StringComparer.OrdinalIgnoreCase);
        var version = args.Contains("--version", StringComparer.OrdinalIgnoreCase)
            || args.Contains("-v", StringComparer.OrdinalIgnoreCase);

        if (version)
        {
            return PrintVersion();
        }

        using var loggerFactory = EngineLogging.CreateConsole(LogLevel.Information);
        var logger = loggerFactory.CreateLogger("DragonISO.Console");

        if (!OperatingSystem.IsWindows())
        {
            logger.LogError("DragonISO.Console requires Windows + the NDI Runtime. Current OS is not Windows.");
            return 1;
        }

        NdiInteropPInvoke interop;
        try
        {
            interop = new NdiInteropPInvoke(loggerFactory.CreateLogger<NdiInteropPInvoke>());
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Could not initialize NDI runtime. Install the NDI Runtime from https://ndi.video/tools/ and try again.");
            return 2;
        }

        if (listSources)
        {
            return await RunListSourcesAsync(interop, logger);
        }

        if (testPattern)
        {
            return await RunTestPatternAsync(interop, logger);
        }

        var configPath = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
            "Dragon-ISO", "config.json");
        var configStore = new ConfigStore(configPath, loggerFactory.CreateLogger<ConfigStore>());

        var probe = new NdiRuntimeProbe(interop, NdiVersion.ExpectedRuntimeVersionPrefix);
        var scaler = new ManagedNearestNeighborFrameScaler();

        // Pipeline factory: wires up real NDI receiver/processor/sender against the production interop.
        IsoPipeline PipelineFactory(IsoPipelineConfig config)
        {
            var clock = new PeriodicTimerFrameClock(config.Settings.FramerateHz);
            return new IsoPipeline(
                config, interop, scaler, clock,
                ExponentialBackoff.Default,
                (delay, ct) => Task.Delay(delay, ct),
                loggerFactory);
        }

        await using var controller = new IsoController(
            interop, PipelineFactory, configStore, probe, loggerFactory);

        // Wire up event printing
        var sub1 = controller.Participants
            .DistinctUntilChanged()
            .Subscribe(plist =>
            {
                logger.LogInformation("Participants ({Count}):", plist.Count);
                foreach (var p in plist)
                    logger.LogInformation("  - {Name}  source={Source}", p.DisplayName, p.CurrentSource?.FullName ?? "<none>");
            });
        var sub2 = controller.Alerts.Subscribe(a => logger.LogWarning("ALERT: {Message}", a.Message));

        using var cts = new CancellationTokenSource();
        SysConsole.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };

        await controller.StartAsync(cts.Token);
        logger.LogInformation("Engine started. Default framerate: {Fps:F2} fps. Press Ctrl+C or 'q' Enter to quit.",
            controller.GlobalSettings.FramerateHz);

        if (enableAll)
        {
            // Auto-enable any participant we see, until cancellation.
            controller.Participants.Subscribe(async plist =>
            {
                foreach (var p in plist.Where(p => p.CurrentSource is not null))
                {
                    try { await controller.EnableIsoAsync(p.Id, customName: null, cts.Token); }
                    catch (Exception ex) { logger.LogWarning(ex, "Could not enable ISO for {Name}.", p.DisplayName); }
                }
            });
        }

        // Read until 'q' or cancellation.
        var input = Task.Run(() =>
        {
            while (!cts.IsCancellationRequested)
            {
                var line = SysConsole.ReadLine();
                if (line is null) return;
                if (line.Trim().Equals("q", StringComparison.OrdinalIgnoreCase))
                {
                    cts.Cancel();
                    return;
                }
            }
        });

        try { await Task.Delay(Timeout.Infinite, cts.Token); }
        catch (OperationCanceledException) { /* expected on quit */ }

        sub1.Dispose();
        sub2.Dispose();
        interop.Dispose();
        logger.LogInformation("Engine stopped.");
        return 0;
    }

    /// <summary>
    /// Prints version + diagnostic info. Always exits 0; the strings are intended to
    /// be pasted into a support ticket.
    /// </summary>
    private static int PrintVersion()
    {
        var asm = typeof(Program).Assembly;
        var asmVersion = asm.GetName().Version?.ToString() ?? "unknown";
        var info = asm.GetCustomAttributes(typeof(System.Reflection.AssemblyInformationalVersionAttribute), false)
            .OfType<System.Reflection.AssemblyInformationalVersionAttribute>()
            .FirstOrDefault()?.InformationalVersion ?? asmVersion;

        SysConsole.WriteLine($"Dragon-ISO Console");
        SysConsole.WriteLine($"  Version:        {info}");
        SysConsole.WriteLine($"  Engine:         {asmVersion} (DragonISO.Engine + DragonISO.Engine.NdiInterop)");
        SysConsole.WriteLine($"  Runtime:        .NET {Environment.Version}");
        SysConsole.WriteLine($"  OS:             {Environment.OSVersion}");
        SysConsole.WriteLine();

        if (OperatingSystem.IsWindows())
        {
            try
            {
                using var lf = EngineLogging.CreateConsole(LogLevel.Warning);
                using var interop = new NdiInteropPInvoke(lf.CreateLogger<NdiInteropPInvoke>());
                SysConsole.WriteLine($"  NDI runtime:    {interop.GetRuntimeVersion()}");
                SysConsole.WriteLine($"  Expects prefix: {NdiVersion.ExpectedRuntimeVersionPrefix}");
            }
            catch (Exception ex)
            {
                SysConsole.WriteLine($"  NDI runtime:    NOT INITIALIZED ({ex.Message})");
            }
        }
        else
        {
            SysConsole.WriteLine($"  NDI runtime:    requires Windows");
        }

        SysConsole.WriteLine();
        SysConsole.WriteLine($"Exit codes:");
        SysConsole.WriteLine($"  0  clean exit");
        SysConsole.WriteLine($"  1  not running on Windows");
        SysConsole.WriteLine($"  2  NDI runtime initialization failed (install from https://ndi.video/tools/)");
        SysConsole.WriteLine();
        SysConsole.WriteLine($"Wild Dragon LLC Â· https://wilddragon.net");
        return 0;
    }

    /// <summary>
    /// Diagnostic mode: enumerates every raw NDI source string visible to the local
    /// NDI finder for ~5 seconds, prints each unique one, then exits. Bypasses the
    /// <see cref="Discovery.NdiSourceParser"/> entirely, so it surfaces sources that
    /// the parser would otherwise reject â€” useful for confirming the on-the-wire
    /// format Teams (or any other producer) is actually emitting.
    /// </summary>
    [SupportedOSPlatform("windows")]
    private static async Task<int> RunListSourcesAsync(NdiInteropPInvoke interop, ILogger logger)
    {
        const int pollMs = 500;
        const int totalMs = 5000;
        logger.LogInformation("Listing raw NDI sources for {TotalMs} ms (polling every {PollMs} ms)â€¦", totalMs, pollMs);
        using var finder = interop.CreateFinder();
        var seen = new SortedSet<string>(StringComparer.Ordinal);
        var elapsed = 0;
        while (elapsed < totalMs)
        {
            await Task.Delay(pollMs);
            elapsed += pollMs;
            foreach (var s in interop.GetCurrentSources(finder))
            {
                if (seen.Add(s))
                    logger.LogInformation("  + {Source}", s);
            }
        }
        logger.LogInformation("Done. {Count} unique source(s) seen in {TotalMs} ms.", seen.Count, totalMs);
        interop.Dispose();
        return 0;
    }

    /// <summary>
    /// Test-pattern mode: spins up an NDI sender named Dragon-ISO_TEST that
    /// broadcasts SMPTE color bars at 1280Ã—720 30fps. Useful for verifying
    /// the NDI runtime + groups + discovery without needing Teams running.
    /// Open Studio Monitor on the same network and you should see the source.
    /// Runs until Ctrl+C.
    /// </summary>
    [SupportedOSPlatform("windows")]
    private static async Task<int> RunTestPatternAsync(NdiInteropPInvoke interop, ILogger logger)
    {
        const int width = 1280;
        const int height = 720;
        const double fps = 30.0;
        const string outputName = "Dragon-ISO_TEST";

        logger.LogInformation("Starting test pattern '{Name}' at {W}Ã—{H}@{Fps} fps. Press Ctrl+C to stop.",
            outputName, width, height, fps);

        using var sender = interop.CreateSender(outputName, groups: null);
        using var cts = new CancellationTokenSource();
        SysConsole.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };

        var frameInterval = TimeSpan.FromSeconds(1.0 / fps);
        long frameNumber = 0;
        var deadline = DateTime.UtcNow + frameInterval;

        try
        {
            while (!cts.IsCancellationRequested)
            {
                var frame = TestPatternGenerator.Render(width, height, frameNumber, DateTime.UtcNow.Ticks);
                interop.SendFrame(sender, frame);
                frameNumber++;
                if (frameNumber % (long)fps == 0)
                {
                    // Heartbeat once per second so the operator can confirm
                    // the loop is alive without flooding the console.
                    logger.LogInformation("Sent {Count} frames", frameNumber);
                }

                // Sleep until the next frame deadline. If we fell behind, just
                // skip the wait and emit immediately â€” no point queueing.
                var wait = deadline - DateTime.UtcNow;
                if (wait > TimeSpan.Zero)
                {
                    try { await Task.Delay(wait, cts.Token); }
                    catch (OperationCanceledException) { break; }
                }
                deadline += frameInterval;
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Test pattern loop crashed.");
            return 3;
        }
        logger.LogInformation("Test pattern stopped. Sent {Count} frames total.", frameNumber);
        return 0;
    }
}
