import type { SidecarCommand, SidecarEvent } from '../protocol';
import type { ChainSource } from './upstream';
import { createSidecarConnector } from '../connect';
import { handleConfigRoute, probe } from './config';
import { handleOfficerRoute } from './routes';
import { getChainSource, getConfig, hasStoreKey } from './upstream';
import { lockAll } from './keys';
import { invalidateAll } from './resolve';
import { API_URL } from '../../officer-url.mjs';

// The officer-wallet sidecar. A bitcoin wallet in the shape Zeus models one — several interchangeable
// backends behind one interface — but server-side, with the key material held here and nowhere else.
//
// WHY THIS PROCESS EXISTS SEPARATELY. Seeds and node credentials never enter the main Officer process.
// The platform is a thin auth proxy (src/servers/api/wallet/router.ts) that forwards to this port and
// holds nothing: no seed, no macaroon, no xpub. Compromising `officer` gets an attacker the ability to
// *call* this sidecar as the authenticated owner — it does not get them a key, and it cannot spend from a
// locked wallet, because a locked wallet has no key material in memory at all.
//
// BACKENDS (mirroring _references/zeus/backends/):
//   onchain   — self-custodial. BIP39 seed sealed under an owner passphrase (keys.ts), BIP84/86/49
//               derivation, Esplora for chain data, bitcoinjs-lib for PSBT construction. This is the
//               server analogue of Zeus's EmbeddedLND/LdkNode, which are native-module-bound and cannot
//               be ported. Watch-only while locked; unlock only to sign.
//   lnd       — LND REST + macaroon                    (ported from backends/LND.ts)
//   cln-rest  — Core Lightning CLNRest + rune          (ported from backends/CLNRest.ts)
//   lndhub    — custodial LNDHub/BlueWallet REST       (ported from backends/LndHub.ts)
//   nwc       — Nostr Wallet Connect, NIP-47           (ported from backends/NostrWalletConnect.ts)
//
// When the owner runs their own node, it registers as an `lnd` or `cln-rest` wallet — no code change.
//
// ─────────────────────────────────────────────────────────────────────────────────────────────────
// HTTP CONTRACT — the platform strips its /api/wallet mount prefix before forwarding.
//
//   GET  /_health                                   ours. Reports network, chain reachability, store key.
//   GET    /_config                                 the owner's chain endpoint (or the built-in default)
//   PUT    /_config                                 {url, kind} — probed as that kind before it is stored
//   DELETE /_config                                 fall back to the built-in default
//   GET  /_officer/config                           network + unlock TTL + storeKeyConfigured
//
//   GET    /_officer/wallets                        list. Never includes secrets.
//   POST   /_officer/wallets                        create. Seeded wallets return the mnemonic ONCE,
//                                                   and only when the sidecar generated it.
//   GET    /_officer/wallets/active                 the currently selected wallet
//   GET    /_officer/wallets/:id                    one wallet
//   PATCH  /_officer/wallets/:id                    rename / defaultBip / config
//   DELETE /_officer/wallets/:id                    {passphrase} required when the wallet holds a seed
//   POST   /_officer/wallets/:id/activate
//
//   GET    /_officer/wallets/:id/lock-state         {hasSeed, unlocked, secondsRemaining}
//   POST   /_officer/wallets/:id/unlock             {passphrase, ttlSec?}
//   POST   /_officer/wallets/:id/lock
//   POST   /_officer/wallets/:id/passphrase         {oldPassphrase, newPassphrase}
//   POST   /_officer/wallets/:id/export-seed        {passphrase} → the mnemonic. Logged as a warning.
//
//   GET    /_officer/wallets/:id/capabilities       what this backend can actually do
//   GET    /_officer/wallets/:id/info               node/chain identity + sync state
//   GET    /_officer/wallets/:id/balances           on-chain confirmed/unconfirmed + lightning local/inbound
//   GET    /_officer/wallets/:id/transactions?limit on-chain history, owner labels overlaid
//   GET    /_officer/wallets/:id/address?peek       fresh receive address
//   GET    /_officer/wallets/:id/utxos              coin control view, freeze flags + labels overlaid
//   POST   /_officer/wallets/:id/utxos/freeze       {outpoint, frozen, reason?}
//   GET    /_officer/wallets/:id/fees               sat/vB estimates
//   POST   /_officer/wallets/:id/send               on-chain spend. Requires an unlocked wallet.
//   GET    /_officer/wallets/:id/invoices?limit
//   POST   /_officer/wallets/:id/invoices           create
//   GET    /_officer/wallets/:id/invoices/:hash     lookup
//   POST   /_officer/wallets/:id/decode             {bolt11}
//   GET    /_officer/wallets/:id/payments?limit
//   POST   /_officer/wallets/:id/pay                {bolt11, amountMsat?, feeLimit…}
//   POST   /_officer/wallets/:id/keysend            {destination, amountMsat}
//   GET    /_officer/wallets/:id/channels
//   GET    /_officer/wallets/:id/peers
//   POST   /_officer/wallets/:id/sign               {message}
//   POST   /_officer/wallets/:id/verify             {message, signature}
//   GET|POST /_officer/wallets/:id/labels           owner annotations for addresses and txids
//
//   anything else                                   404
//
// Operations a backend cannot perform return 501 with code NOT_SUPPORTED, checked against its declared
// capability set before dispatch — never a confusing upstream error. A locked wallet returns 423
// WALLET_LOCKED from signing paths only; every read above keeps working.
// ─────────────────────────────────────────────────────────────────────────────────────────────────

/** Grab an ephemeral free port by briefly binding one and releasing it. */
function getFreePort(): number {
  const probeServer = Bun.serve({ port: 0, hostname: '127.0.0.1', fetch: () => new Response('') });
  const p = probeServer.port;
  probeServer.stop(true);
  if (p == null) throw new Error('failed to acquire a free port');
  return p;
}

const port = getFreePort();

const server = Bun.serve({
  port,
  hostname: '127.0.0.1',
  // Wallet payloads are small — PSBTs and invoices, never file uploads. A tight cap is free hardening.
  maxRequestBodySize: 1 * 1024 * 1024,
  // Bun's default is 10s, which is SHORTER than the Esplora client's own 20s per-request timeout. That
  // inversion is why a slow or unreachable upstream showed up as a wallet with no data at all rather than
  // as an error: Bun killed the response before the scan could either finish or report why it hadn't.
  // A gap-limit scan is many sequential rounds, so the ceiling has to sit above the whole walk, not one
  // request. Bun caps this at 255s; every other sidecar already uses that.
  idleTimeout: 255,
  async fetch(req) {
    const url = new URL(req.url);

    // `/_config` is the chain endpoint, which is per-owner — so unlike /_health it needs the header.
    if (url.pathname === '/_config' || url.pathname.startsWith('/_config/')) {
      const officerUser = req.headers.get('X-Officer-User');
      const userId = Number(officerUser);
      if (!officerUser || !Number.isInteger(userId) || userId <= 0) {
        return Response.json({ error: 'missing or invalid X-Officer-User' }, { status: 401 });
      }
      try {
        return await handleConfigRoute(req, userId, url.pathname.slice('/_config'.length));
      } catch (err) {
        console.error(`[wallet] ${req.method} ${url.pathname} failed`, err instanceof Error ? err.message : err);
        return Response.json({ error: 'internal error' }, { status: 500 });
      }
    }

    if (url.pathname === '/_health') {
      const cfg = getConfig();
      // Health has to probe the endpoint the wallets ACTUALLY use, not the built-in default — reporting
      // the default green while the owner's own indexer is down would be worse than no health at all.
      // The header is optional here (health predates it), so fall back rather than 401.
      const healthUser = Number(req.headers.get('X-Officer-User'));
      const chain: ChainSource =
        Number.isInteger(healthUser) && healthUser > 0
          ? await getChainSource(healthUser)
          : { kind: 'esplora', baseUrl: cfg.esploraUrl, custom: false };

      // Same probe the config form runs, so "saved successfully" and "healthy" can never disagree about
      // what a working endpoint is — including the network cross-check on NBXplorer.
      const result = await probe(chain.baseUrl, chain.kind);

      return Response.json(
        {
          ok: result.ok,
          network: cfg.network,
          kind: chain.kind,
          esplora: chain.baseUrl,
          custom: chain.custom,
          blockHeight: result.blockHeight ?? null,
          ...(result.error ? { error: result.error } : {}),
          // Surfaced because wallet creation is refused without it, and that failure would otherwise
          // look like a bug rather than a missing config line.
          storeKeyConfigured: hasStoreKey(),
          ms: result.ms,
        },
        { status: result.ok ? 200 : 502 },
      );
    }

    if (url.pathname.startsWith('/_officer/')) {
      try {
        const res = await handleOfficerRoute(req, url);
        if (res) return res;
        return Response.json({ error: 'not found' }, { status: 404 });
      } catch (err) {
        // Method and path only. Bodies on this sidecar carry passphrases and mnemonics.
        console.error(`[wallet] ${req.method} ${url.pathname} failed`, err instanceof Error ? err.message : err);
        return Response.json({ error: 'internal error' }, { status: 500 });
      }
    }

    return Response.json({ error: 'not found' }, { status: 404 });
  },
});

// No esplora URL in the banner: it is per-owner state read from the database now, not a boot constant.
console.log(`[wallet] listening on 127.0.0.1:${port} — network=${getConfig().network}`);
if (!hasStoreKey()) {
  console.warn('[wallet] the secret store is unreachable — wallet creation will be refused until it is');
}

type ReplyFn = (msg: SidecarEvent) => void;

function handleCommand(cmd: SidecarCommand, reply: ReplyFn) {
  switch (cmd.type) {
    case 'ping':
      reply({ type: 'pong', id: cmd.id });
      break;
    default:
      reply({
        type: 'error',
        id: (cmd as SidecarCommand).id,
        error: `Unknown command type: ${(cmd as Record<string, unknown>).type}`,
      });
  }
}

const connection = createSidecarConnector({
  apiUrl: `${API_URL}/api/sidecar/register`,
  name: 'wallet',
  capabilities: ['wallet'],
  onCommand(cmd, reply) {
    handleCommand(cmd as SidecarCommand, reply as ReplyFn);
  },
  onConnected() {
    connection.send({ type: 'wallet:server', port });
    console.log(`[wallet] reported server port ${port} to API`);
  },
});

function shutdown(signal: string) {
  console.log(`[wallet] ${signal} received, locking all wallets and shutting down...`);
  // Wipe key material before anything else. This is best-effort — see the caveat in keys.ts — but it
  // costs nothing and closes the obvious window on a graceful restart.
  lockAll();
  invalidateAll();
  try {
    server.stop(true);
  } catch {
    /* already stopped */
  }
  connection.destroy();
  process.exit(0);
}

process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
