// The wallet sidecar's backend contract — a typed re-statement of what Zeus's utils/BackendUtils.ts
// dispatches to duck-typed across its seven backends.
//
// Two things are deliberately NOT copied from Zeus:
//
//   1. Zeus's `call(funcName, args)` returns `false` when a backend lacks a method, so an unsupported
//      feature and a typo'd method name are indistinguishable at the call site. Here the surface is a
//      TypeScript interface and capability negotiation is explicit via `supports()`.
//   2. Zeus threads URL path segments through the interface — `decodePaymentRequest(urlParams[0])`,
//      `closeChannel(urlParams)`. Every operation below takes a named request object.
//
// UNITS. Amounts are satoshis as `number` (max 2.1e15, safely inside 2^53) and millisatoshis as
// decimal `string` (2.1e18 overflows a double). Never widen a msat to a number.

export type BackendKind = 'onchain' | 'lnd' | 'cln-rest' | 'lndhub' | 'nwc';

export type BitcoinNetwork = 'bitcoin' | 'testnet' | 'signet' | 'regtest';

/**
 * Explicit capability flags, replacing Zeus's ~60 `supportsX()` predicates (BackendUtils.ts:200-270).
 * A backend declares these once; routes.ts checks before dispatching so an unsupported operation is a
 * clean 501 rather than a mystery failure deep in an upstream call.
 */
export type Capability =
  // on-chain
  | 'onchainReceive'
  | 'onchainSend'
  | 'coinControl'
  | 'psbt'
  | 'bumpFee'
  | 'sweep'
  | 'accounts'
  // lightning
  | 'lightningReceive'
  | 'lightningSend'
  | 'keysend'
  | 'customPreimages'
  | 'offers'
  // node operation
  | 'channels'
  | 'peers'
  | 'routing'
  | 'signMessage';

// ── node / balances ──────────────────────────────────────────────────────────────────────────────

export type NodeInfo = {
  kind: BackendKind;
  /** Node pubkey, or null for backends with no node identity of their own (onchain, lndhub). */
  pubkey: string | null;
  alias: string | null;
  /** Upstream implementation version, verbatim. Null when the backend cannot report one. */
  version: string | null;
  network: BitcoinNetwork;
  blockHeight: number | null;
  synced: boolean;
};

export type Balances = {
  /** Confirmed on-chain, in sats. Includes anything frozen — see `onchainFrozen`. */
  onchainConfirmed: number;
  onchainUnconfirmed: number;
  /**
   * How much of `onchainConfirmed` sits in UTXOs the owner froze, and so cannot be spent.
   *
   * A component of the confirmed figure rather than a deduction from it, because the coins are still
   * the wallet's — freezing is a spending policy, not a loss. Officer's overlay: freezing lives in
   * Postgres and no backend knows about it, so every backend reports 0 and the route fills it in.
   * Without this the UI shows a balance a max-value send then refuses to spend, with no visible reason.
   */
  onchainFrozen: number;
  /** Sum of local balance across active channels, in sats. Null when the backend has no channels. */
  lightningBalance: number | null;
  /** Sum of remote balance — i.e. inbound liquidity — in sats. */
  lightningInbound: number | null;
};

// ── on-chain ─────────────────────────────────────────────────────────────────────────────────────

export type AddressType = 'p2wpkh' | 'p2tr' | 'p2sh-p2wpkh' | 'p2pkh';

/**
 * BIP44 purpose → the script type it derives. Keyed by the string form, because that is how a wallet's
 * `xpubs` map arrives from the database (JSON object keys are always strings).
 */
export const BIP_ADDRESS_TYPE: Record<string, AddressType> = {
  '44': 'p2pkh',
  '49': 'p2sh-p2wpkh',
  '84': 'p2wpkh',
  '86': 'p2tr',
};

/** Narrow caller-supplied text to an AddressType, or undefined if it names none. */
export function asAddressType(value: string): AddressType | undefined {
  return value === 'p2wpkh' || value === 'p2tr' || value === 'p2sh-p2wpkh' || value === 'p2pkh' ? value : undefined;
}

export type OnchainTx = {
  txid: string;
  /** Net effect on this wallet in sats — negative for a spend. */
  amount: number;
  feeSats: number | null;
  blockHeight: number | null;
  /** Unix seconds. Null while unconfirmed on backends that do not timestamp the mempool. */
  timestamp: number | null;
  confirmations: number;
  label: string | null;
  destAddresses: string[];
  /** Raw tx hex when the backend supplies it, for RBF/inspection. */
  rawHex: string | null;
};

export type Utxo = {
  txid: string;
  vout: number;
  amountSats: number;
  address: string;
  addressType: AddressType | null;
  confirmations: number;
  /** BIP32 path relative to the account xpub, when this UTXO belongs to a derived address. */
  derivationPath: string | null;
  /** Frozen UTXOs are excluded from automatic coin selection. */
  frozen: boolean;
};

export type FeeEstimates = {
  /** sat/vB, keyed by confirmation target in blocks. */
  fastestFee: number;
  halfHourFee: number;
  hourFee: number;
  economyFee: number;
  minimumFee: number;
};

export type NewAddressRequest = {
  type?: AddressType;
  /** Return the current unused address instead of advancing the derivation index. */
  peek?: boolean;
};

export type SendCoinsRequest = {
  address: string;
  /** Ignored when `sendAll` is set. */
  amountSats?: number;
  sendAll?: boolean;
  satPerVbyte: number;
  /** Coin control: restrict inputs to these outpoints (`txid:vout`). Requires the `coinControl` capability. */
  outpoints?: string[];
  spendUnconfirmed?: boolean;
  /** Opt out of RBF signalling. Default is replaceable. */
  rbf?: boolean;
  label?: string;
  /**
   * Outpoints automatic selection must not touch. SET BY THE ROUTE FROM OFFICER'S FROZEN LIST, never by
   * the caller — whatever arrives in the request body is overwritten.
   *
   * Freezing is Officer policy stored in Postgres, and a backend has no business reading that table; so
   * the frozen set has to travel with the request. Note it only ever restricts what may be spent, so
   * even a caller who did smuggle a value in could not widen the selection.
   */
  frozenOutpoints?: readonly string[];
};

export type SendCoinsResult = {
  txid: string;
  feeSats: number;
  rawHex: string | null;
};

// ── lightning ────────────────────────────────────────────────────────────────────────────────────

export type InvoiceState = 'open' | 'settled' | 'canceled' | 'accepted' | 'expired';

export type Invoice = {
  /** Payment hash, hex. */
  paymentHash: string;
  /** BOLT11 payment request. */
  bolt11: string;
  amountMsat: string | null;
  amountPaidMsat: string | null;
  memo: string | null;
  state: InvoiceState;
  createdAt: number;
  expiresAt: number | null;
  settledAt: number | null;
  /** Only ever populated for invoices this wallet created and only when explicitly requested. */
  preimage: string | null;
  isKeysend: boolean;
  isAmp: boolean;
};

export type CreateInvoiceRequest = {
  amountMsat?: string;
  memo?: string;
  expirySeconds?: number;
  /** Requires the `customPreimages` capability. */
  preimage?: string;
  isAmp?: boolean;
  private?: boolean;
};

export type DecodedInvoice = {
  bolt11: string;
  paymentHash: string;
  amountMsat: string | null;
  description: string | null;
  destination: string;
  timestamp: number;
  expiry: number;
  cltvExpiry: number | null;
  routeHints: boolean;
  features: string[];
};

export type PaymentStatus = 'pending' | 'succeeded' | 'failed';

export type Payment = {
  paymentHash: string;
  preimage: string | null;
  amountMsat: string;
  feeMsat: string;
  status: PaymentStatus;
  createdAt: number;
  destination: string | null;
  memo: string | null;
  /** Upstream failure reason, verbatim, when status is 'failed'. */
  failureReason: string | null;
};

export type PayInvoiceRequest = {
  bolt11: string;
  /** Required when the invoice is zero-amount; rejected otherwise. */
  amountMsat?: string;
  /** Absolute cap in msat. Mutually exclusive with feeLimitPercent. */
  feeLimitMsat?: string;
  feeLimitPercent?: number;
  timeoutSeconds?: number;
};

export type KeysendRequest = {
  destination: string;
  amountMsat: string;
  feeLimitMsat?: string;
  message?: string;
};

// ── channels / peers ─────────────────────────────────────────────────────────────────────────────

export type Channel = {
  channelId: string;
  channelPoint: string | null;
  remotePubkey: string;
  remoteAlias: string | null;
  capacitySats: number;
  localBalanceSats: number;
  remoteBalanceSats: number;
  active: boolean;
  private: boolean;
  /** 'open' | 'pending-open' | 'pending-close' | 'force-closing' | 'closed' */
  status: string;
};

export type Peer = {
  pubkey: string;
  address: string;
  alias: string | null;
  inbound: boolean;
};

// ── signing ──────────────────────────────────────────────────────────────────────────────────────

export type SignMessageResult = { signature: string };
export type VerifyMessageResult = { valid: boolean; pubkey: string | null };

// ── the interface ────────────────────────────────────────────────────────────────────────────────

/**
 * Every method may throw `BackendError`. Methods guarded by a capability must only be called after
 * `supports()` returns true — the base class throws `notSupported()` otherwise, so a missed guard is a
 * loud 501 rather than Zeus's silent `false`.
 */
export interface WalletBackend {
  readonly kind: BackendKind;

  supports(cap: Capability): boolean;

  getInfo(): Promise<NodeInfo>;
  getBalances(): Promise<Balances>;

  // on-chain
  getTransactions(opts?: { limit?: number }): Promise<OnchainTx[]>;
  getNewAddress(req?: NewAddressRequest): Promise<{ address: string; type: AddressType }>;
  getUtxos(): Promise<Utxo[]>;
  estimateFees(): Promise<FeeEstimates>;
  sendCoins(req: SendCoinsRequest): Promise<SendCoinsResult>;

  // lightning
  getInvoices(opts?: { limit?: number }): Promise<Invoice[]>;
  createInvoice(req: CreateInvoiceRequest): Promise<Invoice>;
  lookupInvoice(paymentHash: string): Promise<Invoice | null>;
  decodeInvoice(bolt11: string): Promise<DecodedInvoice>;
  getPayments(opts?: { limit?: number }): Promise<Payment[]>;
  payInvoice(req: PayInvoiceRequest): Promise<Payment>;
  sendKeysend(req: KeysendRequest): Promise<Payment>;

  // node operation
  getChannels(): Promise<Channel[]>;
  getPeers(): Promise<Peer[]>;

  signMessage(message: string): Promise<SignMessageResult>;
  verifyMessage(message: string, signature: string): Promise<VerifyMessageResult>;

  /**
   * How old the data this backend just served is. Optional because it is only meaningful for a backend
   * that caches — a node backend answers from its own live state, and reporting a sync age for it would
   * be inventing one.
   */
  getSyncState?(): SyncState;

  /**
   * Ask the chain to search for this wallet's coins from scratch. Resolves as soon as the search is
   * queued, not when it finishes — progress is read back through `getSyncState().rescan`.
   *
   * Optional because only a wallet-level chain source can do it. Absent means the backend has nothing to
   * rescan (a node backend already knows its own coins) or the source has no such endpoint.
   */
  startRescan?(opts?: RescanOptions): Promise<RescanState>;

  /**
   * Re-attach to a rescan that is running upstream but was not started by this process.
   *
   * A deep rescan takes minutes and lives on the indexer, not here — so restarting the sidecar mid-scan
   * used to lose the wallet's only handle on it. `getSyncState().rescan` read back null, the UI showed no
   * scan at all, and the refresh chained onto its completion went with it, leaving the balance stale
   * until the cache TTL happened to expire. This asks the upstream what is actually in flight.
   *
   * Resolves null when nothing is running. Costs an upstream call, so callers reach for it only when
   * there is no local state to report.
   */
  adoptRescan?(): Promise<RescanState | null>;
}

/**
 * Freshness of a cached view of the chain, so the UI can say "3 minutes ago" and, more importantly, tell
 * "this wallet is empty" apart from "we could not ask". Those two rendered identically before this
 * existed, which is the whole reason it does.
 */
export type SyncState = {
  /** Unix ms of the last successful chain read. Null when the chain has never been read successfully. */
  syncedAt: number | null;
  /** The served data is past its refresh interval; a refresh is already running behind this response. */
  stale: boolean;
  /** The last refresh failure, still reported while the previous good data is being served. */
  lastError: string | null;
  /**
   * Whether this chain source can search the chain from scratch at all. False for Esplora, which asks
   * about every address on every refresh and so has nothing to catch up on.
   *
   * Distinct from `rescan === null`, which is ALSO what a source that simply hasn't run one yet reports
   * — conflating the two put a button that 501s in front of every Esplora wallet.
   */
  canRescan: boolean;
  /** A deep rescan in flight, or the outcome of the last one. Null from a source that cannot rescan. */
  rescan: RescanState | null;
};

/**
 * How far a deep rescan should look, when the default is not far enough.
 *
 * The default gap limit is 1000 consecutive unused addresses, which covers any wallet a person has used
 * by hand. It does not cover one restored from a wallet that issued addresses in bulk — and there is no
 * way to detect that case, because the whole problem is that the coins beyond the gap are invisible.
 * Raising it costs the node CPU on one scan; leaving it too low costs the owner the coins. So the knob is
 * exposed rather than tuned, and the caller who knows their history is the one who sets it.
 */
export type RescanOptions = { gapLimit?: number };

/**
 * A deep chain rescan — the upstream searching the whole UTXO set for a wallet's coins, rather than
 * indexing forward from the moment the wallet was registered.
 *
 * This exists for exactly one situation, and it is not a rare one: an imported seed on a node that only
 * started watching the xpub today. Every read succeeds, nothing errors, and the balance is zero — which
 * is indistinguishable from an empty wallet unless the UI can say "still looking". So the state is
 * carried on SyncState beside `syncedAt`, on the same responses, for the same reason.
 *
 * It runs one account variant at a time because the node runs `scantxoutset` one at a time; four
 * variants is minutes, not seconds.
 */
export type RescanState = {
  running: boolean;
  /** Account variants finished, out of how many were queued. */
  done: number;
  total: number;
  /** The variant the node is chewing on right now. Null when nothing is running. */
  current: AddressType | null;
  /** UTXOs the finished variants pulled in. */
  found: number;
  startedAt: number;
  finishedAt: number | null;
  error: string | null;
};

// ── errors ───────────────────────────────────────────────────────────────────────────────────────

export class BackendError extends Error {
  constructor(
    message: string,
    readonly status: number = 502,
    readonly code?: string,
  ) {
    super(message);
    this.name = 'BackendError';
  }
}

/** The wallet holds a seed but is currently locked — signing is impossible until /unlock. */
export class WalletLockedError extends BackendError {
  constructor() {
    super('wallet is locked', 423, 'WALLET_LOCKED');
    this.name = 'WalletLockedError';
  }
}
