using System.Collections.Concurrent;
using System.Diagnostics;
using RioJoy.Core.Protocol;
using RioJoy.Core.Serial;

namespace RioSerialMonitor;

/// <summary>
/// Scripted in-memory RIO for <c>--mash --selftest</c>: proves the mash-test
/// instrumentation (wedge alarm, revival classification, counter delta) works
/// before it is trusted to judge firmware at the cabinet.
///
/// Timeline (seconds from open):
///   0.0 – 4.0   analog replies every 50 ms (healthy)
///   4.0 – 6.7   analog SILENT (the wedge; alarm must fire at ~6.0 with the
///               default 2 s threshold) while the port stays open
///   6.5 / 7.0   ButtonPressed / ButtonReleased 0x05 (masher still mashing)
///   6.7 –  end  analog resumes 200 ms after the button — the tool must
///               classify the wedge as "button-revived" (unpatched signature)
/// CheckRequest is answered with RestartCount 3 / FullBufferCount 1 on the
/// first ask and 7 / 2 afterwards, so the counter delta must read +4 / +1.
/// Expected run outcome: 1 wedge event, verdict FAIL (button-revived), exit 1.
/// </summary>
internal sealed class SelftestTransport : IRioTransport
{
    private readonly ConcurrentQueue<byte[]> _rx = new();
    private readonly SemaphoreSlim _rxReady = new(0);
    private readonly CancellationTokenSource _cts = new();
    private readonly Stopwatch _clock = Stopwatch.StartNew();
    private int _checkCalls;

    public SelftestTransport() => _ = ProduceAsync(_cts.Token);

    public string Description => "selftest (scripted in-memory RIO)";

    public async Task<int> ReadAsync(byte[] buffer, CancellationToken cancellationToken)
    {
        using var linked = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cts.Token);
        await _rxReady.WaitAsync(linked.Token).ConfigureAwait(false);
        if (!_rx.TryDequeue(out byte[]? chunk))
            return 0;
        Array.Copy(chunk, buffer, chunk.Length);
        return chunk.Length;
    }

    public Task WriteAsync(byte[] data, CancellationToken cancellationToken)
    {
        // Answer the requests the test sends; swallow analog polls + lamp writes.
        if (data.Length > 0)
        {
            if (data[0] == (byte)RioCommand.VersionRequest)
            {
                Enqueue(PacketBuilder.Build(RioCommand.VersionReply, new byte[] { 4, 2 }));
            }
            else if (data[0] == (byte)RioCommand.CheckRequest)
            {
                bool first = Interlocked.Increment(ref _checkCalls) == 1;
                Enqueue(PacketBuilder.Build(RioCommand.CheckReply,
                    new[] { (byte)RioStatusType.RestartCount, first ? (byte)3 : (byte)7 }));
                Enqueue(PacketBuilder.Build(RioCommand.CheckReply,
                    new[] { (byte)RioStatusType.AbandonCount, (byte)0 }));
                Enqueue(PacketBuilder.Build(RioCommand.CheckReply,
                    new[] { (byte)RioStatusType.FullBufferCount, first ? (byte)1 : (byte)2 }));
            }
        }
        return Task.CompletedTask;
    }

    private async Task ProduceAsync(CancellationToken ct)
    {
        bool pressSent = false, releaseSent = false;
        var analogPayload = new byte[10]; // all axes zero — valid sample

        while (!ct.IsCancellationRequested)
        {
            await Task.Delay(50, ct).ConfigureAwait(false);
            double t = _clock.Elapsed.TotalSeconds;

            bool inGap = t >= 4.0 && t < 6.7;
            if (!inGap)
                Enqueue(PacketBuilder.Build(RioCommand.AnalogReply, analogPayload));

            if (!pressSent && t >= 6.5)
            {
                pressSent = true;
                Enqueue(PacketBuilder.Build(RioCommand.ButtonPressed, new byte[] { 0x05 }));
            }
            if (!releaseSent && t >= 7.0)
            {
                releaseSent = true;
                Enqueue(PacketBuilder.Build(RioCommand.ButtonReleased, new byte[] { 0x05 }));
            }
        }
    }

    private void Enqueue(byte[] packet)
    {
        _rx.Enqueue(packet);
        _rxReady.Release();
    }

    public void Dispose()
    {
        _cts.Cancel();
        _rxReady.Release(); // wake a pending read so the loop can wind down
        _cts.Dispose();
    }
}
