// Shared types/constants for the /wallet workspace panels.
//
// The wire shapes mirror src/servers/sidecar/wallet/types.ts and the WalletSummary projection in
// src/databases/officer_db/src/wallet/queries.ts. They are restated here rather than imported because the
// officerdev workspace has no path into src/servers — pulling the sidecar's module graph into the browser
// bundle would drag bitcoinjs-lib and the key handling along with it, which is exactly what must never
// reach the client. Keep this file in step with those two by hand; the field names are identical on
// purpose so a diff is a grep.
//
// UNITS — the one rule that matters. Satoshis are `number` (2.1e15, comfortably inside 2^53).
// Millisatoshis are decimal STRINGS (2.1e18 overflows a double) and must never be parsed into a JS
// number. format.ts does msat arithmetic on the digit string; use it rather than reaching for Number().

export const WALLET_SECTIONS = [
  { id: 'overview', label: 'Overview' },
  { id: 'receive', label: 'Receive' },
  { id: 'send', label: 'Send' },
  { id: 'transactions', label: 'Transactions' },
  { id: 'coins', label: 'Coins' },
  { id: 'lightning', label: 'Lightning' },
  { id: 'settings', label: 'Settings' },
] as const;

export type WalletSectionId = (typeof WALLET_SECTIONS)[number]['id'];

/** Where /wallet lands, and where an unrecognised section redirects to. */
export const DEFAULT_WALLET_SECTION: WalletSectionId = 'overview';

export const isWalletSection = (value: string | undefined): value is WalletSectionId =>
  WALLET_SECTIONS.some((s) => s.id === value);

/**
 * The one place the section URL is spelled, so the nav, the guard and any deep link cannot drift apart.
 * Which wallet is open rides along as `?wallet=<id>` — a query param rather than a second path segment,
 * because it is orthogonal to the section and every panel reads it independently.
 */
export const walletSectionPath = (id: WalletSectionId, walletId?: number | null) =>
  walletId == null ? `/wallet/${id}` : `/wallet/${id}?wallet=${walletId}`;

/** The search-param name holding the open wallet. Never holds anything secret — just a row id. */
export const WALLET_PARAM = 'wallet';

// ── Domain objects ────────────────────────────────────────────────────────────────────────────────

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

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

/** Mirrors `Capability` in the sidecar. The UI shows a control only when its capability is present. */
export type Capability =
  | 'onchainReceive'
  | 'onchainSend'
  | 'coinControl'
  | 'psbt'
  | 'bumpFee'
  | 'sweep'
  | 'accounts'
  | 'lightningReceive'
  | 'lightningSend'
  | 'keysend'
  | 'customPreimages'
  | 'offers'
  | 'channels'
  | 'peers'
  | 'routing'
  | 'signMessage';

/** The browser-safe wallet projection — no config, no seed envelope, ever. */
export type WalletSummary = {
  id: number;
  name: string;
  kind: BackendKind;
  network: string;
  fingerprint: string | null;
  xpubs: Record<string, string> | null;
  defaultBip: number;
  isActive: boolean;
  /** Whether this wallet holds a seed at all — i.e. whether unlock/lock apply to it. */
  hasSeed: boolean;
  createdAt: string;
};

export type WalletConfig = {
  network: BitcoinNetwork;
  unlockTtlSec: number;
  /** Wallet creation is refused without it, so the UI blocks the form rather than failing on submit. */
  storeKeyConfigured: boolean;
};

export type LockState = {
  hasSeed: boolean;
  unlocked: boolean;
  secondsRemaining: number;
};

export type NodeInfo = {
  kind: BackendKind;
  pubkey: string | null;
  alias: string | null;
  version: string | null;
  network: BitcoinNetwork;
  blockHeight: number | null;
  synced: boolean;
};

export type Balances = {
  onchainConfirmed: number;
  onchainUnconfirmed: number;
  /**
   * The part of `onchainConfirmed` the owner has frozen. Already counted in it, not deducted — the coins
   * are still the wallet's; freezing is a spending policy. Subtract it to get what a send can actually
   * use, which is what "Spendable" has to mean or a max-value send fails for no visible reason.
   */
  onchainFrozen: number;
  /** Null when the backend has no channels of its own. */
  lightningBalance: number | null;
  lightningInbound: number | null;
};

/**
 * How old the numbers alongside this are, from a backend that caches its view of the chain. Null from a
 * node backend, which answers from its own live state and has no age to report.
 *
 * `syncedAt: null` with a `lastError` is the case worth rendering carefully: the wallet has never been
 * read successfully, so a zero balance means "unknown", not "empty".
 */
export type SyncState = {
  /** Unix ms of the last successful chain read. */
  syncedAt: number | null;
  stale: boolean;
  lastError: string | null;
  /**
   * Whether the chain source can search the chain from scratch at all — false for Esplora, which reads
   * every address on every refresh and has nothing to catch up on. Gate the control on this, NOT on
   * `rescan === null`: that is also what a source reports before its first scan.
   */
  canRescan: boolean;
  /** A deep rescan in flight, or the last one's outcome. Null from a chain source that cannot rescan. */
  rescan: RescanState | null;
};

/**
 * The node searching its whole UTXO set for this wallet's coins.
 *
 * Only ever non-null on NBXplorer, and it exists because an imported wallet on a freshly-tracked xpub
 * reads as a confident zero: nothing errors, the balance is just empty until the node has looked. Four
 * script variants, one at a time, minutes not seconds — which is precisely why it has to be on screen.
 */
export type RescanState = {
  running: boolean;
  done: number;
  total: number;
  current: AddressType | null;
  found: number;
  startedAt: number;
  finishedAt: number | null;
  error: string | null;
};

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

/** What each script type is actually called out loud, for anything the owner reads rather than greps. */
export const ADDRESS_TYPE_LABELS: Record<AddressType, string> = {
  p2wpkh: 'native segwit',
  'p2sh-p2wpkh': 'wrapped segwit',
  p2pkh: 'legacy',
  p2tr: 'taproot',
};

export type OnchainTx = {
  txid: string;
  /** Net effect on this wallet in sats — negative for a spend. */
  amount: number;
  feeSats: number | null;
  blockHeight: number | null;
  timestamp: number | null;
  confirmations: number;
  label: string | null;
  destAddresses: string[];
  rawHex: string | null;
};

/** The sidecar overlays Officer's own `frozen` flag and address label onto the backend's UTXO. */
export type Utxo = {
  txid: string;
  vout: number;
  amountSats: number;
  address: string;
  addressType: AddressType | null;
  confirmations: number;
  derivationPath: string | null;
  frozen: boolean;
  label: string | null;
};

export type FeeEstimates = {
  fastestFee: number;
  halfHourFee: number;
  hourFee: number;
  economyFee: number;
  minimumFee: number;
};

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

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

export type Invoice = {
  paymentHash: string;
  bolt11: string;
  amountMsat: string | null;
  amountPaidMsat: string | null;
  memo: string | null;
  state: InvoiceState;
  createdAt: number;
  expiresAt: number | null;
  settledAt: number | null;
  preimage: string | null;
  isKeysend: boolean;
  isAmp: 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;
  failureReason: string | null;
};

export type Channel = {
  channelId: string;
  channelPoint: string | null;
  remotePubkey: string;
  remoteAlias: string | null;
  capacitySats: number;
  localBalanceSats: number;
  remoteBalanceSats: number;
  active: boolean;
  private: boolean;
  status: string;
};

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

// ── Capability-driven section visibility ──────────────────────────────────────────────────────────

/**
 * Which sections a backend can actually serve. Anything not listed here is hidden from the nav rather
 * than rendered as a control that returns 501 — see the sidecar's `requireCap`.
 *
 * `overview`, `transactions` and `settings` are unconditional: getInfo/getBalances/getTransactions are
 * on the base interface with no capability guard, and settings is wallet lifecycle, not a backend call.
 */
export function sectionAvailable(section: WalletSectionId, caps: readonly Capability[]): boolean {
  const has = (c: Capability) => caps.includes(c);
  switch (section) {
    case 'receive':
      return has('onchainReceive') || has('lightningReceive');
    case 'send':
      return has('onchainSend') || has('lightningSend');
    case 'coins':
      return has('coinControl');
    case 'lightning':
      return has('lightningReceive') || has('lightningSend') || has('channels') || has('peers');
    default:
      return true;
  }
}

export const KIND_LABELS: Record<BackendKind, string> = {
  onchain: 'On-chain (self-custodial)',
  lnd: 'LND',
  'cln-rest': 'Core Lightning',
  lndhub: 'LNDHub',
  nwc: 'Nostr Wallet Connect',
};

/** Networks other than mainnet get a visible badge — sending testnet coins by mistake is a bad day. */
export const NETWORK_TONES: Record<string, string> = {
  bitcoin: 'bg-amber-500/15 text-amber-600',
  testnet: 'bg-emerald-500/15 text-emerald-600',
  signet: 'bg-violet-500/15 text-violet-600',
  regtest: 'bg-slate-500/15 text-slate-600',
};
