using NBitcoin;
using PalladiumWallet.Core.Chain;
using PalladiumWallet.Core.Crypto;
using PalladiumWallet.Core.Net;
using PalladiumWallet.Core.Spv;
using PalladiumWallet.Core.Storage;
using PalladiumWallet.Core.Wallet;

// Wallet CLI (blueprint §13): Core use cases exposed via command line,
// for scripting, testing and comparison against the reference wallet.

try
{
    return args switch
    {
        ["newseed", .. var rest] => NewSeed(rest),
        ["addresses", var words, .. var rest] => Addresses(words, rest),
        ["create", .. var rest] => Create(rest),
        ["restore", var words, .. var rest] => Restore(words, rest),
        ["restore-xpub", var xpub, .. var rest] => RestoreXpub(xpub, rest),
        ["restore-address", var addrs, .. var rest] => RestoreAddress(addrs, rest),
        ["info", .. var rest] => Info(rest),
        ["sync", .. var rest] => await Sync(rest),
        ["send", .. var rest] => await Send(rest),
        ["reset-certs", .. var rest] => ResetCerts(rest),
        ["servers", .. var rest] => await Servers(rest),
        _ => Usage(),
    };
}
catch (Exception ex) when (ex is WalletSpendException or WrongPasswordException
    or CertificatePinMismatchException or ElectrumServerException or SpvVerificationException)
{
    Console.Error.WriteLine($"Error: {ex.Message}");
    return 1;
}

static int NewSeed(string[] o)
{
    var length = Opt(o, "--words") == "24" ? MnemonicLength.TwentyFour : MnemonicLength.Twelve;
    Console.WriteLine(Bip39.Generate(length));
    return 0;
}

static int Addresses(string words, string[] o)
{
    var account = AccountFromWords(words, o);
    if (account is null)
        return 1;
    var count = int.TryParse(Opt(o, "--count"), out var n) ? n : 5;
    Console.WriteLine($"network: {account.Profile.NetName}   kind: {account.Kind}   path: m/{account.AccountPath}");
    Console.WriteLine($"account: {account.ToSlip132()}");
    for (var i = 0; i < count; i++)
        Console.WriteLine($"  receiving/{i}: {account.GetReceiveAddress(i)}");
    for (var i = 0; i < Math.Min(count, 2); i++)
        Console.WriteLine($"  change/{i}:    {account.GetChangeAddress(i)}");
    return 0;
}

static int Create(string[] o)
{
    var mnemonic = Bip39.Generate(Opt(o, "--words") == "24" ? MnemonicLength.TwentyFour : MnemonicLength.Twelve);
    Console.WriteLine("New mnemonic (write it on paper, it is NOT shown again):");
    Console.WriteLine($"  {mnemonic}");
    return SaveWallet(mnemonic.ToString(), o);
}

static int Restore(string words, string[] o)
{
    if (!Bip39.TryParse(words, out _))
    {
        Console.Error.WriteLine("Invalid mnemonic (wrong words or checksum).");
        return 1;
    }
    return SaveWallet(words.Trim(), o);
}

static int RestoreXpub(string xpubText, string[] o)
{
    var profile = Profile(o);
    if (!Slip132.TryDecodePublic(xpubText, profile, out var xpub, out var kind))
    {
        Console.Error.WriteLine("Extended key not recognised for this network.");
        return 1;
    }
    var account = HdAccount.FromAccountXpub(xpub!, kind, profile);
    var doc = new WalletDocument
    {
        Network = profile.NetName,
        ScriptKind = kind.ToString(),
        AccountPath = account.AccountPath.ToString(),
        AccountXpub = account.ToSlip132(),
    };
    var path = WalletPath(o, profile);
    WalletStore.Save(doc, path, Opt(o, "--password"));
    Console.WriteLine($"Watch-only wallet saved to {path}");
    return 0;
}

static int RestoreAddress(string addrsText, string[] o)
{
    var profile = Profile(o);
    var addresses = addrsText.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
    if (addresses.Length == 0)
    {
        Console.Error.WriteLine("At least one address is required.");
        return 1;
    }
    WalletDocument doc;
    try
    {
        (doc, _) = WalletLoader.NewFromAddresses(addresses, profile);
    }
    catch (InvalidDataException ex)
    {
        Console.Error.WriteLine(ex.Message);
        return 1;
    }
    var path = WalletPath(o, profile);
    WalletStore.Save(doc, path, Opt(o, "--password"));
    Console.WriteLine($"Watch-only wallet saved to {path} ({addresses.Length} address(es), cannot sign)");
    return 0;
}

static int Info(string[] o)
{
    var (doc, account, path) = OpenWallet(o);
    Console.WriteLine($"file:     {path}");
    Console.WriteLine($"network:  {doc.Network}   kind: {doc.ScriptKind}   watch-only: {doc.IsWatchOnly}");
    Console.WriteLine($"path:     m/{doc.AccountPath}");
    Console.WriteLine($"xpub:     {doc.AccountXpub}");
    if (doc.Cache is { } cache)
    {
        Console.WriteLine($"balance:  {CoinAmount.Format(cache.SpendableSats, account.Profile.CoinUnit)} spendable"
            + (cache.ImmatureSats != 0 ? $" + {CoinAmount.Format(cache.ImmatureSats)} maturing (not spendable)" : "")
            + (cache.PendingVerificationSats != 0 ? $" + {CoinAmount.Format(cache.PendingVerificationSats)} awaiting SPV verification (not spendable)" : "")
            + (cache.UnconfirmedSats != 0 ? $" + {CoinAmount.Format(cache.UnconfirmedSats)} pending confirmation (not spendable)." : ""));
        Console.WriteLine($"sync:     height {cache.TipHeight}, {cache.History.Count} transactions");
        Console.WriteLine($"receive:  {account.GetReceiveAddress(cache.NextReceiveIndex)}");
    }
    else
    {
        Console.WriteLine($"receive:  {account.GetReceiveAddress(0)}   (never synchronized)");
    }

    if (o.Contains("--addresses") && doc.Cache is { } c)
    {
        Console.WriteLine("addresses:");
        foreach (var a in c.Addresses)
            Console.WriteLine($"  {(a.IsChange ? "change " : "receive")}{a.Index,3}  {a.Address}  " +
                $"{CoinAmount.Format(a.BalanceSats),18}  ({a.TxCount} tx)");
    }
    return 0;
}

static async Task<int> Sync(string[] o)
{
    var (doc, account, path) = OpenWallet(o);
    await using var client = await Connect(o, account.Profile);

    var sync = new WalletSynchronizer(account, client, doc.GapLimit);
    sync.Progress += msg => Console.WriteLine($"  {msg}");
    var net = PalladiumNetworks.For(account.Profile.Kind);
    sync.PreloadCaches(
        doc.Cache?.RawTxHex ?? [],
        doc.Cache?.VerifiedAt ?? [],
        doc.Cache?.BlockHeaders,
        doc.Cache?.NextReceiveIndex ?? 0,
        doc.Cache?.NextChangeIndex ?? 0,
        net,
        doc.Cache?.AnchoredUpTo);
    var result = await sync.SyncOnceAsync();

    var (rawHex, verifiedAt, blockHeaders, anchoredUpTo) = sync.ExportCaches(net);
    doc.Cache = new SyncCache
    {
        TipHeight = result.TipHeight,
        ConfirmedSats = result.ConfirmedSats,
        UnconfirmedSats = result.UnconfirmedSats,
        ImmatureSats = result.ImmatureSats,
        PendingVerificationSats = result.PendingVerificationSats,
        SpendableSats = result.SpendableSats,
        NextReceiveIndex = result.NextReceiveIndex,
        NextChangeIndex = result.NextChangeIndex,
        History = [.. result.History],
        Utxos = [.. result.Utxos],
        Addresses = [.. result.AddressRows],
        RawTxHex = rawHex,
        VerifiedAt = verifiedAt,
        BlockHeaders = blockHeaders,
        AnchoredUpTo = anchoredUpTo,
    };
    WalletStore.Save(doc, path, Opt(o, "--password"));

    Console.WriteLine($"Balance: {CoinAmount.Format(result.SpendableSats, account.Profile.CoinUnit)} spendable"
        + (result.ImmatureSats != 0 ? $" + {CoinAmount.Format(result.ImmatureSats)} maturing (not spendable)" : "")
        + (result.PendingVerificationSats != 0 ? $" + {CoinAmount.Format(result.PendingVerificationSats)} awaiting SPV verification (not spendable)" : "")
        + (result.UnconfirmedSats != 0 ? $" + {CoinAmount.Format(result.UnconfirmedSats)} pending confirmation (not spendable)" : ""));
    Console.WriteLine($"History ({result.History.Count}):");
    foreach (var tx in result.History)
        Console.WriteLine($"  {(tx.Height > 0 ? tx.Height.ToString() : "mempool"),7}  " +
            $"{(tx.DeltaSats >= 0 ? "+" : "")}{CoinAmount.Format(tx.DeltaSats)}  {tx.Txid}" +
            (tx.Verified ? "" : "  (unverified)"));
    return 0;
}

static async Task<int> Send(string[] o)
{
    var (doc, account, path) = OpenWallet(o);
    if (doc.Cache is null)
    {
        Console.Error.WriteLine("Wallet never synchronized: run 'sync' first.");
        return 1;
    }
    var to = Opt(o, "--to") ?? throw new WalletSpendException("--to missing.");
    var destination = BitcoinAddress.Create(to, PalladiumNetworks.For(account.Profile.Kind));
    var sendAll = o.Contains("--all");
    long amount = 0;
    if (!sendAll && !CoinAmount.TryParseCoins(Opt(o, "--amount") ?? "", out amount))
        throw new WalletSpendException("--amount missing or invalid (or use --all).");
    var feeRate = decimal.TryParse(Opt(o, "--feerate"), out var fr) ? fr : 1m;

    // Source transactions of the UTXOs: needed for amounts and signing.
    await using var client = await Connect(o, account.Profile);
    var network = PalladiumNetworks.For(account.Profile.Kind);
    var transactions = new Dictionary<string, Transaction>();
    foreach (var txid in doc.Cache.Utxos.Select(u => u.Txid).Distinct())
        transactions[txid] = Transaction.Parse(await client.GetTransactionAsync(txid), network);

    var built = new TransactionFactory(account).Build(
        doc.Cache.Utxos, transactions, destination, amount, feeRate,
        doc.Cache.NextChangeIndex, doc.Cache.TipHeight, sendAll);

    Console.WriteLine($"txid:  {built.Txid}");
    Console.WriteLine($"fee:   {CoinAmount.Format(built.Fee.Satoshi, account.Profile.CoinUnit)} " +
        $"({feeRate} sat/vB, {built.Transaction.GetVirtualSize()} vB)");

    if (!built.Signed)
    {
        Console.WriteLine("Watch-only wallet: PSBT to sign offline (§6.5):");
        Console.WriteLine(built.Psbt.ToBase64());
        return 0;
    }

    if (!o.Contains("--broadcast"))
    {
        Console.WriteLine("Signed transaction (NOT broadcast, add --broadcast):");
        Console.WriteLine(built.ToHex());
        return 0;
    }

    var txid2 = await client.BroadcastAsync(built.ToHex());
    Console.WriteLine($"Broadcast: {txid2}");
    return 0;
}

static int ResetCerts(string[] o)
{
    var profile = Profile(o);
    new CertificatePinStore(AppPaths.CertificatePinsPath(profile.Kind)).ResetAll();
    Console.WriteLine($"Saved SSL certificates for {profile.NetName} cleared.");
    return 0;
}

static async Task<int> Servers(string[] o)
{
    var profile = Profile(o);
    var registry = new ServerRegistry(profile, AppPaths.ServersPath(profile.Kind));

    if (o.Contains("--discover"))
    {
        await using var client = await Connect(o, profile);
        var added = await registry.DiscoverAsync(client);
        Console.WriteLine($"Discovered {added} new servers from peers.");
    }

    Console.WriteLine($"Known servers ({profile.NetName}):");
    foreach (var server in registry.All)
        Console.WriteLine($"  {server}");
    return 0;
}

// ----- shared helpers -----

static ChainProfile Profile(string[] o) => Opt(o, "--net") switch
{
    "testnet" => ChainProfiles.Testnet,
    "regtest" => ChainProfiles.Regtest,
    _ => ChainProfiles.Mainnet,
};

static ScriptKind Kind(string[] o) => Opt(o, "--kind") switch
{
    "legacy" => ScriptKind.Legacy,
    "wrapped" => ScriptKind.WrappedSegwit,
    _ => ScriptKind.NativeSegwit,
};

static string WalletPath(string[] o, ChainProfile profile) =>
    Opt(o, "--file") ?? AppPaths.DefaultWalletPath(profile.Kind);

static HdAccount? AccountFromWords(string words, string[] o)
{
    if (!Bip39.TryParse(words, out var mnemonic))
    {
        Console.Error.WriteLine("Invalid mnemonic (wrong words or checksum).");
        return null;
    }
    var profile = Profile(o);
    var kind = Kind(o);
    var passphrase = Opt(o, "--passphrase");
    return Opt(o, "--path") is { } path
        ? HdAccount.FromSeed(Bip39.ToSeed(mnemonic!, passphrase), kind, profile, KeyPath.Parse(path))
        : HdAccount.FromMnemonic(mnemonic!, passphrase, kind, profile);
}

static int SaveWallet(string words, string[] o)
{
    var profile = Profile(o);
    var (doc, account) = WalletLoader.NewFromMnemonic(
        words, Opt(o, "--passphrase"), Kind(o), profile,
        Opt(o, "--path") is { } p ? KeyPath.Parse(p) : null);
    var password = Opt(o, "--password");
    if (string.IsNullOrEmpty(password))
        Console.WriteLine("WARNING: no --password, the seed will be stored in plaintext on disk (§17).");
    var path = WalletPath(o, profile);
    WalletStore.Save(doc, path, password);
    Console.WriteLine($"Wallet saved to {path}");
    Console.WriteLine($"First address: {account.GetReceiveAddress(0)}");
    return 0;
}

static (WalletDocument, IWalletAccount, string) OpenWallet(string[] o)
{
    var path = WalletPath(o, Profile(o));
    if (!WalletStore.Exists(path))
        throw new WalletSpendException($"No wallet at {path}: use 'create' or 'restore'.");
    var doc = WalletStore.Load(path, Opt(o, "--password"));
    return (doc, WalletLoader.ToAccount(doc), path);
}

static async Task<ElectrumClient> Connect(string[] o, ChainProfile profile)
{
    var useSsl = o.Contains("--ssl");
    string host;
    int port;
    if (Opt(o, "--server") is { } server)
    {
        var parts = server.Split(':');
        host = parts[0];
        port = parts.Length > 1 ? int.Parse(parts[1])
            : useSsl ? profile.DefaultSslPort : profile.DefaultTcpPort;
    }
    else
    {
        // Without --server, use the first known server (bootstrap §3 or discovered §9).
        var known = new ServerRegistry(profile, AppPaths.ServersPath(profile.Kind)).Default
            ?? throw new WalletSpendException("No known server for this network: use --server host:port.");
        host = known.Host;
        port = known.PortFor(useSsl);
    }

    var pins = new CertificatePinStore(AppPaths.CertificatePinsPath(profile.Kind));
    Console.WriteLine($"Connecting to {host}:{port}{(useSsl ? " (TLS)" : "")}…");
    return await ElectrumClient.ConnectAsync(host, port, useSsl, pins);
}

static string? Opt(string[] options, string name)
{
    var i = Array.IndexOf(options, name);
    return i >= 0 && i + 1 < options.Length ? options[i + 1] : null;
}

static int Usage()
{
    Console.WriteLine("""
        PalladiumWallet CLI

        Wallet:
          create        [--words 12|24] [--kind segwit|wrapped|legacy] [--net mainnet|testnet|regtest]
                        [--passphrase W] [--password P] [--file PATH]
          restore       "<mnemonic>" [same options as create] [--path m/...]
          restore-xpub  <slip132 xpub> [--net ...] [--password P] [--file PATH]   (watch-only)
          restore-address <addr1,addr2,...> [--net ...] [--password P] [--file PATH]   (watch-only, no keys)
          info          [--net ...] [--password P] [--file PATH]

        Network (indexing server; without --server the first known server is used):
          sync          [--server host[:port]] [--ssl] [--net ...] [--password P] [--file PATH]
          send          --to ADDRESS (--amount X | --all) [--feerate sat/vB]
                        [--server host[:port]] [--ssl] [--broadcast] [...]
          servers       [--discover] [--server host[:port]] [--ssl] [--net ...]
          reset-certs   [--net ...]

        Tools:
          newseed       [--words 12|24]
          addresses     "<mnemonic>" [--kind ...] [--net ...] [--count N] [--passphrase W] [--path m/...]
        """);
    return 1;
}
