using Nfcaime.Cli.Pn532;
using System.IO.Ports;

namespace Nfcaime.Cli;

internal static class Program
{
    private const string DefaultPort = "COM14";
    private const int DefaultBaud = 115200;
    private const int DefaultRetryCount = 2;
    private const int DefaultTimeoutMs = 1500;

    private static int Main(string[] args)
    {
        if (!TryParseArgs(args, out var options, out var errorMessage))
        {
            Console.Error.WriteLine(errorMessage);
            return 1;
        }

        Console.WriteLine($"Mode: {options.Mode}");
        Console.WriteLine($"Port: {options.Port}");
        Console.WriteLine($"Baud: {options.Baud}");
        Console.WriteLine($"Retries: {options.Retries}");
        Console.WriteLine($"TimeoutMs: {options.TimeoutMs}");

        if (options.Mode == "replay") RunReplay(options);
        else if (options.Mode == "diag") RunDiag(options);
        else RunLive(options);

        return 0;
    }

    private static bool TryParseArgs(string[] args, out CliOptions options, out string errorMessage)
    {
        var parsed = new CliOptions { Port = DefaultPort, Baud = DefaultBaud, Mode = "live" };
        for (var index = 0; index < args.Length; index++)
        {
            var arg = args[index];
            if (!arg.StartsWith("--", StringComparison.Ordinal)) { errorMessage = $"Unknown argument: {arg}"; options = default!; return false; }
            var equalsIndex = arg.IndexOf('=');
            var name = equalsIndex >= 0 ? arg[..equalsIndex] : arg;
            var value = equalsIndex >= 0 ? arg[(equalsIndex + 1)..] : (index + 1 < args.Length && !args[index + 1].StartsWith("--") ? args[++index] : null);
            switch (name)
            {
                case "--port": parsed.Port = value ?? DefaultPort; break;
                case "--baud": if (int.TryParse(value, out var baud)) parsed.Baud = baud; break;
                case "--mode": parsed.Mode = value?.ToLowerInvariant() ?? "live"; break;
                case "--retries": if (int.TryParse(value, out var retries)) parsed.Retries = retries; break;
                case "--timeoutMs": if (int.TryParse(value, out var timeoutMs)) parsed.TimeoutMs = timeoutMs; break;
                default: return Fail($"Unknown argument: {name}", out options, out errorMessage);
            }
        }
        options = parsed; errorMessage = string.Empty; return true;
    }

    private static bool Fail(string message, out CliOptions options, out string errorMessage)
    {
        options = default!; errorMessage = message; return false;
    }

    private sealed class CliOptions { public string Port { get; set; } = DefaultPort; public int Baud { get; set; } = DefaultBaud; public string Mode { get; set; } = "live"; public string? ReplayFile { get; set; } public bool ReplayCorrupt { get; set; } public int Retries { get; set; } = DefaultRetryCount; public int TimeoutMs { get; set; } = DefaultTimeoutMs; }

    private static void RunLive(CliOptions options)
    {
        var timeout = TimeSpan.FromMilliseconds(options.TimeoutMs);
        using var transport = new SerialFrameTransport(options.Port, options.Baud, timeout);
        using var session = new Pn532Session(transport, timeout, options.Retries);
        session.Open();
        try
        {
            var result = RunPn532FelicaFlow(session);
            Console.WriteLine($"CardId: {Convert.ToHexString(result.CardId)}");
            Console.WriteLine($"AccessCode: {result.AccessCode}");
        }
        finally { session.Close(); }
    }

    private static void RunReplay(CliOptions options)
    {
        var timeout = TimeSpan.FromMilliseconds(options.TimeoutMs);
        var frames = LoadReplayFrames(options.ReplayFile!);
        using var transport = new ReplayFrameTransport(frames, options.ReplayCorrupt);
        using var session = new Pn532Session(transport, timeout, options.Retries);
        session.Open();
        try
        {
            var result = RunPn532FelicaFlow(session);
            Console.WriteLine($"CardId: {Convert.ToHexString(result.CardId)}");
            Console.WriteLine($"AccessCode: {result.AccessCode}");
        }
        finally { session.Close(); }
    }

    private static void RunDiag(CliOptions options)
    {
        using var serial = new SerialPort(options.Port, options.Baud)
        {
            ReadTimeout = 500,
            WriteTimeout = 500,
            DtrEnable = true,
            RtsEnable = false
        };
        serial.Open();
        serial.DtrEnable = true;
        serial.RtsEnable = false;
        Thread.Sleep(250);
        var wakeup = new byte[] { 0x55, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
        serial.Write(wakeup, 0, wakeup.Length);
        Thread.Sleep(50);
        var fw = Pn532HsuFrame.BuildDataFrame(Pn532HsuFrame.HostToPn532Tfi, new byte[] { 0x02 });
        serial.Write(fw, 0, fw.Length);
        Thread.Sleep(200);
        var buf = new byte[serial.BytesToRead];
        serial.Read(buf, 0, buf.Length);
        Console.WriteLine($"RX: {Convert.ToHexString(buf)}");
    }

    private enum CardKind { Felica, MifareClassic }
    private sealed record CardTarget(CardKind Kind, byte Tg, byte[] CardId);
    private sealed record FlowResult(byte[] CardId, string AccessCode);

    private static FlowResult RunPn532FelicaFlow(Pn532Session session)
    {
        ExpectPn532ResponseCode(session.SendCommand(new byte[] { 0x02 }), expectedResponseCode: 0x03);
        ExpectPn532StatusOk(session.SendCommand(new byte[] { 0x14, 0x01 }), expectedResponseCode: 0x15);
        ExpectPn532StatusOk(session.SendCommand(new byte[] { 0x32, 0x01, 0x03 }), expectedResponseCode: 0x33);

        var target = WaitForCard(session);
        Thread.Sleep(100);

        if (target.Kind == CardKind.Felica)
        {
            Console.WriteLine($"Card detected! IDm: {Convert.ToHexString(target.CardId)}");
            var readCmd = FelicaCommandBuilder.BuildReadWithoutEncryptionCommand(target.CardId);
            var readResponse = SendInDataExchange(session, target.Tg, readCmd, TimeSpan.FromSeconds(5));
            var spad0 = FelicaResponseParser.ParseSpad0(readResponse);
            var decryptor = new FeliCaDecryptor();
            var decrypted = decryptor.Decrypt(spad0);
            var accessCode = AccessCodeFormatter.ToAccessCodeString(decrypted);
            return new FlowResult(target.CardId, accessCode);
        }

        Console.WriteLine($"Card detected! TypeA UID: {Convert.ToHexString(target.CardId)}");
        var m1AccessCode = TryReadMifareClassicAccessCode(session, target.Tg, target.CardId);
        if (m1AccessCode is null)
        {
            throw new InvalidOperationException("Failed to read Mifare Classic AccessCode: no key matched sector 0.");
        }

        return new FlowResult(target.CardId, m1AccessCode);
    }

    private static CardTarget WaitForCard(Pn532Session session)
    {
        var warned = false;
        while (true)
        {
            var f212 = session.SendCommand(new byte[] { 0x4A, 0x01, 0x01, 0x00, 0xFF, 0xFF, 0x01, 0x00 }, TimeSpan.FromMilliseconds(500));
            if (ExtractFeliCaTarget(f212, out var target)) return target;

            var a106 = session.SendCommand(new byte[] { 0x4A, 0x01, 0x00 }, TimeSpan.FromMilliseconds(600));
            if (ExtractTypeATarget(a106, out target))
            {
                return target;
            }

            if (!warned) { Console.WriteLine("Waiting for Aime/FeliCa or M1 card..."); warned = true; }
            Thread.Sleep(100);
        }
    }

    private static bool ExtractFeliCaTarget(Pn532FrameParseResult result, out CardTarget target)
    {
        target = null;
        if (result.Kind == Pn532FrameKind.Data && result.Payload.Length >= 15 && result.Payload[0] == 0x4B && result.Payload[1] > 0)
        {
            var tg = result.Payload[2];
            var idm = result.Payload.AsSpan(5, 8).ToArray();
            target = new CardTarget(CardKind.Felica, tg, idm);
            return true;
        }
        return false;
    }

    private static bool ExtractTypeATarget(Pn532FrameParseResult result, out CardTarget target)
    {
        target = null;
        if (result.Kind == Pn532FrameKind.Data && result.Payload.Length >= 8 && result.Payload[0] == 0x4B && result.Payload[1] > 0)
        {
            var tg = result.Payload[2];
            var uidLen = result.Payload[6];
            var uidOffset = 7;
            if (uidLen > 0 && result.Payload.Length >= uidOffset + uidLen)
            {
                var uid = result.Payload.AsSpan(uidOffset, uidLen).ToArray();
                target = new CardTarget(CardKind.MifareClassic, tg, uid);
                return true;
            }
        }
        return false;
    }

    private static string? TryReadMifareClassicAccessCode(Pn532Session session, byte tg, ReadOnlySpan<byte> uid)
    {
        if (uid.Length < 4)
        {
            throw new InvalidOperationException("Mifare Classic UID is shorter than 4 bytes.");
        }

        const byte blockNumber = 2; // sector 0 block 2
        var uid4 = uid[..4];

        foreach (var keyHex in MifareClassicKeys)
        {
            var key = Convert.FromHexString(keyHex);
            if (TryMifareAuthenticate(session, tg, blockNumber, keyTypeA: true, key, uid4) ||
                TryMifareAuthenticate(session, tg, blockNumber, keyTypeA: false, key, uid4))
            {
                var block = ReadMifareBlock(session, tg, blockNumber);
                var hex = Convert.ToHexString(block);
                return hex.Length <= 20 ? hex : hex[^20..];
            }
        }

        return null;
    }

    private static bool TryMifareAuthenticate(
        Pn532Session session,
        byte tg,
        byte blockNumber,
        bool keyTypeA,
        ReadOnlySpan<byte> key,
        ReadOnlySpan<byte> uid4
    )
    {
        var cmd = new byte[2 + 1 + 1 + 6 + 4];
        cmd[0] = 0x40;
        cmd[1] = tg;
        cmd[2] = keyTypeA ? (byte)0x60 : (byte)0x61;
        cmd[3] = blockNumber;
        key.CopyTo(cmd.AsSpan(4, 6));
        uid4.CopyTo(cmd.AsSpan(10, 4));

        var response = session.SendCommand(cmd, TimeSpan.FromMilliseconds(1000));
        if (response.Kind != Pn532FrameKind.Data || response.Payload.Length < 2 || response.Payload[0] != 0x41)
        {
            return false;
        }

        return response.Payload[1] == 0x00;
    }

    private static byte[] ReadMifareBlock(Pn532Session session, byte tg, byte blockNumber)
    {
        var cmd = new byte[] { 0x40, tg, 0x30, blockNumber };
        var response = ExpectPn532ResponseCode(session.SendCommand(cmd, TimeSpan.FromMilliseconds(1200)), expectedResponseCode: 0x41);
        if (response.Payload.Length < 2 || response.Payload[1] != 0x00)
        {
            throw new InvalidOperationException($"Mifare read block failed: status=0x{(response.Payload.Length > 1 ? response.Payload[1] : 0):X2}");
        }

        var data = response.Payload.Length == 2 ? Array.Empty<byte>() : response.Payload[2..];
        if (data.Length < 16)
        {
            throw new InvalidOperationException($"Mifare read block returned {data.Length} bytes, expected at least 16.");
        }

        return data[..16];
    }

    private static readonly string[] MifareClassicKeys =
    [
        "6090D00632F5","019761AA8082","574343467632","A99164400748","62742819AD7C","CC5075E42BA1",
        "B9DF35A0814C","8AF9C718F23D","58CD5C3673CB","FC80E88EB88C","7A3CDAD7C023","30424C029001",
        "024E4E44001F","ECBBFA57C6AD","4757698143BD","1D30972E6485","F8526D1A8D6D","1300EC8C7E80",
        "F80A65A87FFA","DEB06ED4AF8E","4AD96BF28190","000390014D41","0800F9917CB0","730050555253",
        "4146D4A956C4","131157FBB126","E69DD9015A43","337237F254D5","9A8389F32FBF","7B8FB4A7100B",
        "C8382A233993","7B304F2A12A6","FC9418BF788B"
    ];

    private static byte[] SendInDataExchange(Pn532Session session, byte tg, ReadOnlySpan<byte> payloadToTarget, TimeSpan? timeout = null)
    {
        var pn532Payload = new byte[2 + payloadToTarget.Length];
        pn532Payload[0] = 0x40; pn532Payload[1] = tg;
        payloadToTarget.CopyTo(pn532Payload.AsSpan(2));
        var response = ExpectPn532ResponseCode(session.SendCommand(pn532Payload, responseTimeout: timeout ?? TimeSpan.FromSeconds(4)), expectedResponseCode: 0x41);
        if (response.Payload.Length < 2 || response.Payload[1] != 0x00) 
            throw new InvalidOperationException($"InDataExchange failed: 0x{(response.Payload.Length > 1 ? response.Payload[1] : 0):X2}");
        return response.Payload.Length == 2 ? Array.Empty<byte>() : response.Payload[2..];
    }

    private static Pn532FrameParseResult ExpectPn532ResponseCode(Pn532FrameParseResult response, byte expectedResponseCode)
    {
        if (response.Kind != Pn532FrameKind.Data) throw new InvalidOperationException($"PN532 error: {response.Kind} ({response.Error}).");
        if (response.Payload.Length < 1 || response.Payload[0] != expectedResponseCode) 
            throw new InvalidOperationException($"Expected 0x{expectedResponseCode:X2}, got 0x{(response.Payload.Length > 0 ? response.Payload[0] : 0):X2}");
        return response;
    }

    private static void ExpectPn532StatusOk(Pn532FrameParseResult response, byte expectedResponseCode)
    {
        response = ExpectPn532ResponseCode(response, expectedResponseCode);
        if (response.Payload.Length >= 2 && response.Payload[1] != 0x00) throw new InvalidOperationException($"Status fail: 0x{response.Payload[1]:X2}");
    }

    private static IReadOnlyList<byte[]> LoadReplayFrames(string path) => File.ReadAllLines(path).Where(l => !string.IsNullOrWhiteSpace(l) && !l.Trim().StartsWith('#')).Select(line => line.Split(' ').Select(s => Convert.ToByte(s, 16)).ToArray()).ToList();
}
