import { pgTable, serial, integer, text, boolean, timestamp, jsonb, uniqueIndex } from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import { users } from '../auth/schema';

// Bitcoin wallets for the officer-wallet sidecar. The owner registers one or more wallets — either a
// self-custodial on-chain wallet whose seed lives here, or a connection to a node (LND / Core Lightning /
// LNDHub / NWC) exactly as Zeus models them — and switches between them.
//
// TWO COLUMNS HOLD SPENDING AUTHORITY AND THEY ARE PROTECTED DIFFERENTLY. This asymmetry is deliberate:
//
//   `config`        — node credentials (macaroon, rune, LNDHub password, NWC URI). Encrypted at rest with
//                     the 'wallet' store key via ../crypto.ts, the way headscale_servers.api_key uses its
//                     own. It CANNOT be
//                     passphrase-protected: background balance polling needs it without the owner present.
//
//   `seed_envelope` — a BIP39 mnemonic that is ALREADY sealed under an owner passphrase by the sidecar
//                     (see servers/sidecar/wallet/keys.ts) before it ever arrives here, and is then
//                     encrypted AGAIN with the 'wallet' store key on the way into this column. Two independent
//                     secrets, neither sufficient alone. A database dump does not spend; a leaked .env
//                     does not spend.
//
// `xpubs` and `fingerprint` are stored in the CLEAR, on purpose. They are what lets the wallet show
// balances, history and fresh receive addresses while locked — the watch-only-when-locked property. An
// xpub leak costs privacy (an observer can enumerate the wallet's addresses), never funds.
//
// Every table is `wallet_`-prefixed and this file holds nothing else, so it can move into
// src/servers/sidecar/wallet/ wholesale when sidecars own their schema. Only officer-wallet reads these.

export const walletWallets = pgTable(
  'wallet_wallets',
  {
    id: serial('id').primaryKey(),
    userId: integer('user_id')
      .notNull()
      .references(() => users.id, { onDelete: 'cascade' }),
    name: text('name').notNull(),
    // BackendKind: 'onchain' | 'lnd' | 'cln-rest' | 'lndhub' | 'nwc'. TS-only enum, no DB CHECK —
    // matches how pipeline_jobs.mode is handled elsewhere in this schema.
    kind: text('kind').notNull(),
    // BitcoinNetwork: 'bitcoin' | 'testnet' | 'signet' | 'regtest'.
    network: text('network').notNull().default('bitcoin'),
    // Backend connection config as JSON, encrypted. Null for a pure on-chain wallet, which connects to
    // nothing but the Esplora endpoint the sidecar is configured with.
    config: text('config'),
    // The passphrase-sealed SeedEnvelope, encrypted again. Null for every remote-node wallet — those
    // hold no seed, the node does.
    seedEnvelope: text('seed_envelope'),
    // BIP32 master fingerprint (8 hex chars), for PSBT construction and hardware-wallet pairing.
    fingerprint: text('fingerprint'),
    // { "44": "xpub…", "49": "ypub…", "84": "zpub…", "86": "xpub…" } — plaintext, see header.
    xpubs: jsonb('xpubs').$type<Record<string, string>>(),
    // Which derivation standard new receive addresses use. 84 (native segwit) is the default.
    defaultBip: integer('default_bip').notNull().default(84),
    isActive: boolean('is_active').notNull().default(false),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
    updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (t) => [
    uniqueIndex('uq_wallet_wallets_user_name').on(t.userId, t.name),
    // At most one active wallet per owner, enforced by the DB rather than convention — a partial unique
    // index over active rows only, mirroring uq_headscale_servers_one_active.
    uniqueIndex('uq_wallet_wallets_one_active')
      .on(t.userId)
      .where(sql`${t.isActive}`),
  ],
);

// Owner-assigned labels for addresses and transactions. Zeus keeps these client-side; here they belong to
// the server so every client sees the same annotations.
export const walletLabels = pgTable(
  'wallet_labels',
  {
    id: serial('id').primaryKey(),
    walletId: integer('wallet_id')
      .notNull()
      .references(() => walletWallets.id, { onDelete: 'cascade' }),
    // 'address' | 'tx'
    kind: text('kind').notNull(),
    // The address string or txid being labelled.
    ref: text('ref').notNull(),
    label: text('label').notNull(),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (t) => [uniqueIndex('uq_wallet_labels_wallet_kind_ref').on(t.walletId, t.kind, t.ref)],
);

// Frozen UTXOs, excluded from automatic coin selection. Separate from labels because this one affects
// what the wallet will actually spend — losing a label is cosmetic, losing a freeze spends a coin the
// owner meant to keep back.
//
// NOTE the contrast with wallet_chain_cache below: this table is POLICY (the owner's decision, only
// they can restate it) and that one is OBSERVATION (the chain's, refetchable at any time). That is why
// only one of them can be safely truncated.
export const walletFrozenUtxos = pgTable(
  'wallet_frozen_utxos',
  {
    id: serial('id').primaryKey(),
    walletId: integer('wallet_id')
      .notNull()
      .references(() => walletWallets.id, { onDelete: 'cascade' }),
    // `txid:vout`
    outpoint: text('outpoint').notNull(),
    reason: text('reason'),
    createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
  },
  (t) => [uniqueIndex('uq_wallet_frozen_utxos_wallet_outpoint').on(t.walletId, t.outpoint)],
);

/**
 * The last thing the chain told us about an on-chain wallet, so a wallet screen paints real numbers
 * immediately instead of nothing while a gap-limit scan runs.
 *
 * Every field is DERIVED and rebuildable — it is recomputed from the account xpubs above on each
 * successful refresh, so this table holds no secret and nothing authoritative. Truncating it costs one
 * slow load and nothing else.
 *
 * Only the chain's own facts are stored. Addresses, scripts and pubkeys are NOT: they are pure EC
 * derivation from the xpub, cheaper to recompute on load than to persist, and re-deriving them means a
 * restored snapshot can never disagree with the wallet's actual keys.
 */
export type WalletChainSnapshot = {
  /** Bumped when the shape below changes; a snapshot at any other version is discarded, not migrated. */
  v: 1;
  tipHeight: number;
  /** One entry per address the scan visited, keyed by its derivation coordinates. */
  addresses: {
    /** AddressType: 'p2wpkh' | 'p2tr' | 'p2sh-p2wpkh' | 'p2pkh'. */
    type: string;
    /** BIP44 chain index: 0 receive, 1 change. */
    chain: number;
    index: number;
    confirmed: number;
    unconfirmed: number;
    txCount: number;
  }[];
  utxos: {
    txid: string;
    vout: number;
    value: number;
    type: string;
    chain: number;
    index: number;
    /** Null while unconfirmed. Confirmations are recomputed against tipHeight, never stored. */
    blockHeight: number | null;
  }[];
  /** The OnchainTx shape from servers/sidecar/wallet/types.ts, restated here so this package imports nothing. */
  transactions: {
    txid: string;
    amount: number;
    feeSats: number | null;
    blockHeight: number | null;
    timestamp: number | null;
    confirmations: number;
    label: string | null;
    destAddresses: string[];
    rawHex: string | null;
  }[];
};

export const walletChainCache = pgTable('wallet_chain_cache', {
  // One row per wallet, so a refresh is a single atomic upsert with no partially-written window.
  walletId: integer('wallet_id')
    .primaryKey()
    .references(() => walletWallets.id, { onDelete: 'cascade' }),
  // Nullable: a wallet whose very first refresh failed has an error to report and no data to report it
  // against, and that row still has to exist for the UI to say why the screen is empty.
  snapshot: jsonb('snapshot').$type<WalletChainSnapshot>(),
  /** When the chain was last read successfully — the age the UI shows. Not when the row was written. */
  syncedAt: timestamp('synced_at', { withTimezone: true }),
  // Kept ALONGSIDE the last good snapshot rather than replacing it. A failure that wiped the data would
  // reproduce the exact bug this table exists to fix: an unreachable upstream rendering as a zero balance
  // instead of as a stale one.
  lastError: text('last_error'),
  lastErrorAt: timestamp('last_error_at', { withTimezone: true }),
});
