using System.Diagnostics;
using RioJoy.Core.Serial;

namespace RioSerialMonitor;

/// <summary>
/// E0-threshold firmware verification (rio-firmware `--e0thresh` images).
///
/// Two phases against a DTR-reset board (counters zeroed, display F0000000):
///  - Phase A (gate hold): analog requests answered NAK-then-ACK — NAKing
///    the COMPLETED reply frame forces exactly one immediate retransmit,
///    ACKing that retransmit ends the cycle. Each event increments the
///    teardown counter $3185, invoking the display renderer sub-threshold.
///    The _e0t5 gate must keep the display F0000000; stock firmware paints
///    E0... at the first event. (Responses must follow the complete frame:
///    the board arms its reply-await only when the frame finishes sending,
///    and once the first retry timeout lapses the cycle runs blind.)
///  - Phase B (flip): analog requests we never ACK — the board runs the
///    full retry cycle (5 retransmits), gives up ($3184++, RESTART $FE)
///    and tears down ($3185++). $3185 crosses the threshold; the display
///    must flip to the E0 readout the tool predicts from observed traffic.
///
/// Bench 2026-07-19 (9600 e0t5 chip): PASS — display held F0000000 through
/// the handshake and all four phase-A teardowns, flipped exactly at the
/// 5th teardown to E0000105 ($3187=00, $3184=01, $3185=05).
///
///     dotnet run --project tools/RioSerialMonitor -- --e0test [port]
///         [--baud rate] [--hold n] [--flip n]
///
/// No --baud probes 9600, 31250, 62500 (DTR-resetting each try). HANDS OFF
/// buttons/keypads during the run — an unACKed button packet shifts the
/// counters (the tool folds observed traffic into its predictions either
/// way). Exit: 0 = ran, 2 = no board reply on any baud.
/// </summary>
internal static class E0Test
{
    private const int Threshold = 5;

    private enum AckMode { Never, Immediate, NakThenAck }

    public static async Task<int> RunAsync(string[] args)
    {
        string port = "COM1";
        int baud = 0, holdEvents = 4, flipEvents = 1;
        for (int i = 0; i < args.Length; i++)
        {
            switch (args[i])
            {
                case "--e0test": break;
                case "--baud" when i + 1 < args.Length && int.TryParse(args[i + 1], out int b): baud = b; i++; break;
                case "--hold" when i + 1 < args.Length && int.TryParse(args[i + 1], out int h) && h >= 0: holdEvents = h; i++; break;
                case "--flip" when i + 1 < args.Length && int.TryParse(args[i + 1], out int f) && f > 0: flipEvents = f; i++; break;
                default:
                    if (!args[i].StartsWith("--")) port = args[i];
                    break;
            }
        }

        var sw = Stopwatch.StartNew();
        object gate = new();
        var buffer = new List<byte>();
        int cursor = 0;

        // Reader-thread ACK automation (latency matters: the board's ACK wait
        // at 9600 is ~4ms/retry, so ACKs must not wait for a polling loop).
        AckMode ackMode = AckMode.Never;
        int replyByte = 0x87;      // frame command byte the current phase expects
        int frameLen = 12;         // full frame: cmd + payload + checksum
        int repliesSeen = 0;       // completed frames of replyByte since phase reset
        int frameCountdown = 0;    // >0: inside a reply frame, bytes remaining
        bool ackSent = false;

        SerialPortTransport? transport = null;
        Task? reader = null;

        void PhaseReset(AckMode mode, int expectReply, int expectLen)
        {
            lock (gate)
            {
                ackMode = mode;
                replyByte = expectReply;
                frameLen = expectLen;
                repliesSeen = 0;
                frameCountdown = 0;
                ackSent = false;
                cursor = buffer.Count;
            }
        }

        int[] bauds = baud != 0 ? new[] { baud } : new[] { 9600, 31250, 62500 };
        (int Major, int Minor)? version = null;

        foreach (int tryBaud in bauds)
        {
            Console.WriteLine($"[{sw.Elapsed.TotalSeconds,6:F2}s] probing {port} @ {tryBaud} (DTR reset, ~2s boot wait)...");
            try { transport = new SerialPortTransport(port, tryBaud); }
            catch (Exception ex)
            {
                Console.WriteLine($"  FAILED to open {port}: {ex.GetType().Name}: {ex.Message}");
                return 2;
            }

            lock (gate) { buffer.Clear(); cursor = 0; }
            SerialPortTransport t = transport;
            reader = Task.Run(async () =>
            {
                var tmp = new byte[256];
                try
                {
                    while (true)
                    {
                        int n = await t.ReadAsync(tmp, CancellationToken.None);
                        if (n <= 0) break;
                        byte? respond = null;
                        lock (gate)
                        {
                            for (int i = 0; i < n; i++)
                            {
                                buffer.Add(tmp[i]);
                                // Frame tracking: respond only once the FULL reply
                                // frame is on the wire � the board arms its
                                // reply-await when the frame finishes sending; a
                                // response mid-frame is ignored.
                                if (frameCountdown > 0)
                                {
                                    if (--frameCountdown > 0) continue;
                                    repliesSeen++;
                                    if (ackMode == AckMode.Immediate && !ackSent)
                                    {
                                        ackSent = true;
                                        respond = 0xFC;
                                    }
                                    else if (ackMode == AckMode.NakThenAck && repliesSeen <= 2)
                                    {
                                        // frame 1 -> NAK (board resends, $3184++),
                                        // frame 2 -> ACK (cycle ends cleanly).
                                        respond = repliesSeen == 1 ? (byte)0xFD : (byte)0xFC;
                                        ackSent = repliesSeen == 2;
                                    }
                                }
                                else if (tmp[i] == replyByte)
                                {
                                    frameCountdown = frameLen - 1;
                                }
                            }
                        }
                        if (respond is byte r)
                            await t.WriteAsync(new byte[] { r }, CancellationToken.None);
                    }
                }
                catch { /* port closed */ }
            });

            await Task.Delay(2000);            // boot: counters cleared, display F0000000

            PhaseReset(AckMode.Immediate, 0x86, 4);   // version reply: cmd+2+ck
            await t.WriteAsync(new byte[] { 0x81, 0x01 }, CancellationToken.None);
            byte[] got = await Collect(1200, b => IndexOf(b, 0x86) >= 0);
            int vi = IndexOf(got, 0x86);
            if (vi >= 0)
            {
                if (vi + 2 < got.Length) version = (got[vi + 1], got[vi + 2]);
                baud = tryBaud;
                break;
            }

            Console.WriteLine($"  no version reply at {tryBaud}");
            transport.Dispose();
            transport = null;
            if (reader is not null) await reader;
        }

        if (transport is null)
        {
            Console.WriteLine("  no board reply on any baud — is the board powered / the right chip in?");
            return 2;
        }

        // Handshake bookkeeping: fold any retries/give-up into the prediction.
        // Bench-calibrated counter semantics (2026-07-19, confirmed on the
        // display): $3184 = timeout-retry cycles (give-ups), $3185 = reply
        // teardowns, i.e. any cycle that needed at least one retransmission.
        await Task.Delay(300);
        byte[] hs = Drain();
        int cycles = SeenAll(0xFE) ? 1 : 0;                       // $3184 prediction
        int teardowns = CountAll(0x86) > 1 ? 1 : 0;               // $3185 prediction

        Console.WriteLine();
        Console.WriteLine($"== E0-threshold test :: {transport.Description}, firmware {version?.Major}.{version?.Minor} ==");
        Console.WriteLine($"   threshold {Threshold}; phase A: {holdEvents} sub-threshold event(s), phase B: {flipEvents} give-up event(s)");
        if (cycles > 0 || teardowns > 0)
            Console.WriteLine($"   note: handshake was not clean (teardowns={teardowns}, give-ups={cycles}); predictions include it.");
        Console.WriteLine("   HANDS OFF buttons/keypads for the whole run.");
        Console.WriteLine();
        string expected = Expected(cycles, teardowns);
        Console.WriteLine($">>> WATCH THE 8-DIGIT DISPLAY. It should read {expected} right now. <<<");
        await Task.Delay(4000);

        int eventNo = 0;
        bool ok = true;

        for (int k = 0; k < holdEvents && ok; k++)
            ok = await RunEvent(AckMode.NakThenAck, "hold");
        for (int k = 0; k < flipEvents && ok; k++)
            ok = await RunEvent(AckMode.Never, "flip");

        Console.WriteLine();
        Console.WriteLine("== done ==");
        Console.WriteLine($"   final predicted counters: $3184=${cycles:X2} $3185=${teardowns:X2} -> display {Expected(cycles, teardowns)}");
        Console.WriteLine("   PASS = display held F0000000 through every sub-threshold line above and");
        Console.WriteLine("          matched the E0 predictions after the threshold crossing.");
        Console.WriteLine("   (Stock firmware flips at the very first retry.) DTR reset restores F0.");

        transport.Dispose();
        if (reader is not null) await reader;
        return 0;

        async Task<bool> RunEvent(AckMode mode, string phase)
        {
            eventNo++;
            PhaseReset(mode, 0x87, 12);   // analog reply: cmd+10+ck
            await transport!.WriteAsync(new byte[] { 0x82, 0x02 }, CancellationToken.None);

            byte[] bytes = mode == AckMode.Never
                ? await Collect(2500, b => IndexOf(b, 0xFE) >= 0)
                : await Collect(800, _ => false);
            await Task.Delay(250);             // let the cycle finish either way
            bytes = Concat(bytes, Drain());

            int transmissions = Count(bytes, 0x87);
            bool sawRestart = IndexOf(bytes, 0xFE) >= 0;
            if (transmissions == 0)
            {
                Console.WriteLine($"[event {eventNo}] NO analog reply ({bytes.Length} bytes) — aborting.");
                return false;
            }

            if (transmissions > 1) teardowns++;          // $3185: imperfect cycle
            if (sawRestart && mode == AckMode.Never) cycles++;  // $3184: give-up

            Console.WriteLine($"[event {eventNo}] {phase}: reply x{transmissions} " +
                              $"({transmissions - 1} retransmit(s)), give-up cycle={mode == AckMode.Never} " +
                              $"-> $3184=${cycles:X2} $3185=${teardowns:X2}");
            Console.WriteLine($"          >>> DISPLAY SHOULD NOW READ: {Expected(cycles, teardowns)} <<<");
            await Task.Delay(4000);
            return true;
        }

        string Expected(int cy, int td) =>
            cy >= Threshold || td >= Threshold ? $"E000{cy:X2}{td:X2}" : "F0000000";

        byte[] Drain()
        {
            lock (gate)
            {
                byte[] r = buffer.Skip(cursor).ToArray();
                cursor = buffer.Count;
                return r;
            }
        }

        int CountAll(byte v) { lock (gate) return buffer.Count(x => x == v); }
        bool SeenAll(byte v) { lock (gate) return buffer.Contains(v); }

        async Task<byte[]> Collect(int ms, Func<byte[], bool> done)
        {
            var end = sw.Elapsed + TimeSpan.FromMilliseconds(ms);
            var all = new List<byte>();
            while (sw.Elapsed < end)
            {
                all.AddRange(Drain());
                if (done(all.ToArray())) break;
                await Task.Delay(15);
            }
            all.AddRange(Drain());
            return all.ToArray();
        }

        static int IndexOf(byte[] a, byte v) => Array.IndexOf(a, v);
        static int Count(byte[] a, byte v) => a.Count(x => x == v);
        static byte[] Concat(byte[] a, byte[] b) => a.Concat(b).ToArray();
    }
}
