using System.CommandLine;
using PCSC;
using PCSC.Monitoring;
using PCSC.Iso7816;

const decimal DefaultAmount = 1.50M;

var rootCommand = new RootCommand("Fancy Test Machine for AfRApay");

var listReadersOption = new Option<bool>("--list-readers", "List card readers and exit");
rootCommand.Add(listReadersOption);

var webAddrOption = new Option<Uri>("--web-addr", "Base URL for AfRApay.Web");
webAddrOption.SetDefaultValue(new Uri("http://127.0.0.1:5296"));
rootCommand.Add(webAddrOption);

rootCommand.SetHandler((listReaders, webAddr) => {
    using (var context = ContextFactory.Instance.Establish(SCardScope.System)) {
        // Ignore Yubikeys.
        var readerNames = context.GetReaders()
            .Where((readerName) => !readerName.Contains("Yubico"))
            .ToArray();

        // We need at least one card reader or this won't work!
        if (readerNames.Length == 0) {
            Console.Error.WriteLine("Error: no card reader detected");
            Environment.Exit(1);
        }

        // If --list-readers is passed, list readers and exit.
        if (listReaders) {
            Console.Error.WriteLine("----------- Connected Readers ----------");
            foreach (var name in readerNames) {
                Console.WriteLine(name);
            }
            Console.Error.WriteLine("----------------------------------------");
            Environment.Exit(0);
        }

        using HttpClient httpClient = new();
        httpClient.BaseAddress = webAddr;

        Console.Error.WriteLine("----------------------------------------");
        Console.Error.WriteLine("--- AfRApay FTM - Fancy Test Machine ---");
        Console.Error.WriteLine("----------------------------------------");
        Console.Error.WriteLine();
        Console.Error.WriteLine("AfRApay.Web: {0}", httpClient.BaseAddress);
        Console.Error.WriteLine();
        Console.Error.WriteLine("Hotkeys (case insensitive):");
        Console.Error.WriteLine("    [-] Debit (default)");
        Console.Error.WriteLine("    [+] Credit");
        Console.Error.WriteLine("    [=] Set amount (default: €1.50)");
        Console.Error.WriteLine();
        Console.Error.WriteLine("    [B] Balance query");
        Console.Error.WriteLine("    [L] Link card (initiate from web UI)");
        Console.Error.WriteLine("  [Esc] Cancel, reset state and amount");
        Console.Error.WriteLine();
        Console.Error.WriteLine("----------------------------------------");


        // Listen for events on all connected readers.
        using (var monitor = MonitorFactory.Instance.Create(SCardScope.System)) {
            var state = TerminalState.Debit;
            decimal amount = DefaultAmount;

            monitor.Initialized += (_, args) => Console.WriteLine("[ Reader Initialized: {0} ]", args.ReaderName);
            monitor.MonitorException += (_, args) => {
                Console.Error.WriteLine("! ERROR: {0}", args);
                Environment.Exit(1);
            };
            monitor.StatusChanged += (_, args) => Console.WriteLine("~ {0} -> {1}", args.LastState, args.NewState);
            monitor.CardInserted += (_, args) => {
                Console.WriteLine("> TAP: {0}", Convert.ToHexString(args.Atr));
                var reader = new IsoReader(context, args.ReaderName, SCardShareMode.Shared, SCardProtocol.Any);
                HandleTap(reader, httpClient, state, amount);
            };
            monitor.CardRemoved += (_, args) => {
                Console.WriteLine("< OFF");
                Console.WriteLine(); // Write a blank line between card taps for readability.
            };

            Console.WriteLine("[ Starting... ]");
            monitor.Start(readerNames);
            while (true) {
                var key = Console.ReadKey();
                var dontPrint = false;
                 switch (key.Key) {
                    case ConsoleKey.Subtract:
                    case ConsoleKey.OemMinus:
                        state = TerminalState.Debit;
                        break;
                    case ConsoleKey.Add:
                    case ConsoleKey.OemPlus:
                        state = TerminalState.Credit;
                        break;
                    case (ConsoleKey)0 when key.KeyChar == '=':
                        Console.Error.Write("\b => ENTER AMOUNT: ");
                        amount = Math.Abs(Decimal.Parse(Console.ReadLine() ?? "1.50".Trim()));
                        break;
                    case ConsoleKey.L:
                        state = TerminalState.Link;
                        break;
                    case ConsoleKey.B:
                        state = TerminalState.Balance;
                        break;
                    case ConsoleKey.Escape:
                        state = TerminalState.Debit;
                        amount = DefaultAmount;
                        break;
                    default:
                        Console.Error.Write("\b");
                        dontPrint = true;
                        break;
                };
                if (dontPrint) {
                    // Invalid input, just ignore it.
                } else if (state == TerminalState.Debit || state == TerminalState.Credit) {
                    Console.Error.WriteLine("\b => {0}: €{1}", state, amount);
                } else {
                    Console.Error.WriteLine("\b => {0}", state);
                }
            }
        }
    }
}, listReadersOption, webAddrOption);

return await rootCommand.InvokeAsync(args);

// Queries a card for data when one is tapped.
static async void HandleTap(IsoReader reader, HttpClient httpClient, TerminalState state, decimal amount) {
    // Send a PCSC pseudo-APDU to query the ISO 14443 UID.
    var uidRsp = reader.Transmit(new CommandApdu(IsoCase.Case2Short, SCardProtocol.Any) {
        CLA = 0xFF,
        Instruction = InstructionCode.GetData,
        P1 = 0x00,
        P2 = 0x00,
    });
    if (!IsSucc(uidRsp)) {
        Console.Error.WriteLine("--> Card Error: SW1={0} SW2={1}", (SW1Code)uidRsp.SW1, uidRsp.SW2);
        return;
    }
    var uid = uidRsp.GetData();
    Console.WriteLine("  UID: {0}", Convert.ToHexString(uid));

    // Query the backend, which endpoint depending on terminal state.
    switch (state) {
        case TerminalState.Debit:
        case TerminalState.Credit:
            var finalAmount = Math.Abs(amount) * (state == TerminalState.Debit ? 1 : -1);
            await CallGet(httpClient, String.Format("/api/card/transaction?card={0}&amount={1}", Convert.ToHexString(uid), finalAmount));
            break;
        case TerminalState.Link:
            await CallGet(httpClient, String.Format("/api/card/link?card={0}", Convert.ToHexString(uid)));
            break;
        case TerminalState.Balance:
            await CallGet(httpClient, String.Format("/api/card/balance?card={0}", Convert.ToHexString(uid)));
            break;
        default:
            Console.Error.WriteLine("UNKNOWN TERMINAL STATE: {0}", state);
            break;
    }
}

// Was the command successful?
static bool IsSucc(Response rsp) {
    return rsp.SW1 == (byte)SW1Code.Normal && rsp.SW2 == 0x00;
}

static async Task<string> CallGet(HttpClient client, string path) {
    Console.WriteLine("  -> GET {0}", path);
    var rsp = await client.GetStringAsync(path);
    Console.WriteLine("  <- {0}", rsp);
    return rsp;
}

// Terminal State.
enum TerminalState {
    Debit,
    Credit,
    Link,
    Balance,
};
