import type { WalletChainSnapshot } from './schema';
import { eq, and, desc } from 'drizzle-orm';
import { db } from '../db';
import { walletWallets, walletLabels, walletFrozenUtxos, walletChainCache } from './schema';
import { encryptSecret, decryptSecret } from '../crypto';

// Wallet access for the officer-wallet sidecar. Callers deal in PLAINTEXT — the at-rest layer ('wallet'
// purpose in the secret store) is
// applied and stripped here, so route handlers never touch crypto. See ../crypto.ts, ../schema/wallet.ts.
//
// Note what "plaintext" means for `seedEnvelope`: it is the passphrase-sealed envelope, which is itself
// still ciphertext. This layer only removes the SECOND wrapping. Nothing in this file can read a mnemonic,
// and that is intentional — only servers/sidecar/wallet/keys.ts can, and only with the owner passphrase.
//
// THREE return types, and the projection is what enforces the separation:
//   WalletSummary  — safe to serialize to the browser. No config, no seed envelope, at all.
//   WalletSecrets  — decrypted config for the sidecar's own upstream calls. Never returned by a handler.
//   SealedSeed     — the sealed envelope, for keys.ts to open with the owner passphrase.
// A bare `select()` would leak both ciphertext columns into every list response the moment someone
// forgot to strip them, so no function here uses one.

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

export type WalletSummary = {
  id: number;
  name: string;
  kind: WalletKind;
  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: Date;
};

export type WalletSecrets = { id: number; kind: WalletKind; network: string; config: Record<string, unknown> | null };

const walletCols = {
  id: walletWallets.id,
  name: walletWallets.name,
  kind: walletWallets.kind,
  network: walletWallets.network,
  fingerprint: walletWallets.fingerprint,
  xpubs: walletWallets.xpubs,
  defaultBip: walletWallets.defaultBip,
  isActive: walletWallets.isActive,
  createdAt: walletWallets.createdAt,
  seedEnvelope: walletWallets.seedEnvelope,
};

function toSummary(row: {
  id: number;
  name: string;
  kind: string;
  network: string;
  fingerprint: string | null;
  xpubs: Record<string, string> | null;
  defaultBip: number;
  isActive: boolean;
  createdAt: Date;
  seedEnvelope: string | null;
}): WalletSummary {
  return {
    id: row.id,
    name: row.name,
    kind: row.kind as WalletKind,
    network: row.network,
    fingerprint: row.fingerprint,
    xpubs: row.xpubs,
    defaultBip: row.defaultBip,
    isActive: row.isActive,
    hasSeed: row.seedEnvelope !== null,
    createdAt: row.createdAt,
  };
}

/** Every wallet the owner has registered, active first then newest. Never includes secrets. */
export async function listWallets(userId: number): Promise<WalletSummary[]> {
  const rows = await db
    .select(walletCols)
    .from(walletWallets)
    .where(eq(walletWallets.userId, userId))
    .orderBy(desc(walletWallets.isActive), desc(walletWallets.createdAt));
  return rows.map(toSummary);
}

export async function getWallet(userId: number, id: number): Promise<WalletSummary | null> {
  const [row] = await db
    .select(walletCols)
    .from(walletWallets)
    .where(and(eq(walletWallets.userId, userId), eq(walletWallets.id, id)));
  return row ? toSummary(row) : null;
}

export async function getActiveWallet(userId: number): Promise<WalletSummary | null> {
  const [row] = await db
    .select(walletCols)
    .from(walletWallets)
    .where(and(eq(walletWallets.userId, userId), eq(walletWallets.isActive, true)));
  return row ? toSummary(row) : null;
}

/** Decrypted backend connection config, for the sidecar's upstream calls. Never leaves the sidecar. */
export async function getWalletSecrets(userId: number, id: number): Promise<WalletSecrets | null> {
  const [row] = await db
    .select({
      id: walletWallets.id,
      kind: walletWallets.kind,
      network: walletWallets.network,
      config: walletWallets.config,
    })
    .from(walletWallets)
    .where(and(eq(walletWallets.userId, userId), eq(walletWallets.id, id)));
  if (!row) return null;
  return {
    id: row.id,
    kind: row.kind as WalletKind,
    network: row.network,
    config: row.config ? (JSON.parse(decryptSecret('wallet', row.config)) as Record<string, unknown>) : null,
  };
}

/**
 * The passphrase-sealed seed envelope, still sealed. Returns null for wallets that hold no seed.
 * The ONLY caller should be the sidecar's unlock path.
 */
export async function getSealedSeed(userId: number, id: number): Promise<string | null> {
  const [row] = await db
    .select({ seedEnvelope: walletWallets.seedEnvelope })
    .from(walletWallets)
    .where(and(eq(walletWallets.userId, userId), eq(walletWallets.id, id)));
  if (!row?.seedEnvelope) return null;
  return decryptSecret('wallet', row.seedEnvelope);
}

export type CreateWalletParams = {
  userId: number;
  name: string;
  kind: WalletKind;
  network: string;
  /** Plaintext; encrypted here. */
  config?: Record<string, unknown> | null;
  /** The already-sealed envelope as JSON; encrypted again here. */
  sealedSeed?: string | null;
  fingerprint?: string | null;
  xpubs?: Record<string, string> | null;
  defaultBip?: number;
  makeActive?: boolean;
};

export async function createWallet(params: CreateWalletParams): Promise<WalletSummary> {
  return db.transaction(async (tx) => {
    if (params.makeActive) {
      await tx
        .update(walletWallets)
        .set({ isActive: false })
        .where(and(eq(walletWallets.userId, params.userId), eq(walletWallets.isActive, true)));
    }
    const [row] = await tx
      .insert(walletWallets)
      .values({
        userId: params.userId,
        name: params.name,
        kind: params.kind,
        network: params.network,
        config: params.config ? encryptSecret('wallet', JSON.stringify(params.config)) : null,
        seedEnvelope: params.sealedSeed ? encryptSecret('wallet', params.sealedSeed) : null,
        fingerprint: params.fingerprint ?? null,
        xpubs: params.xpubs ?? null,
        defaultBip: params.defaultBip ?? 84,
        isActive: params.makeActive ?? false,
      })
      .returning(walletCols);
    return toSummary(row!);
  });
}

/**
 * Mutable wallet metadata. CANNOT TOUCH THE SEED — see replaceSealedSeed below for why that is a
 * separate function rather than one more optional field here.
 */
export async function updateWallet(
  userId: number,
  id: number,
  patch: { name?: string; config?: Record<string, unknown>; defaultBip?: number },
): Promise<WalletSummary | null> {
  const set: Record<string, unknown> = { updatedAt: new Date() };
  if (patch.name !== undefined) set.name = patch.name;
  if (patch.defaultBip !== undefined) set.defaultBip = patch.defaultBip;
  if (patch.config !== undefined) set.config = encryptSecret('wallet', JSON.stringify(patch.config));

  const [row] = await db
    .update(walletWallets)
    .set(set)
    .where(and(eq(walletWallets.userId, userId), eq(walletWallets.id, id)))
    .returning(walletCols);
  return row ? toSummary(row) : null;
}

/**
 * Overwrite the encrypted seed. The ONLY caller is the passphrase change, which reseals the same words
 * under a new passphrase and has already proved knowledge of the old one.
 *
 * It is a function of its own, rather than a fourth field on updateWallet, because it was one: the PATCH
 * route typed its request body as `{name, defaultBip, config}` and passed the parsed object straight
 * through, and a TypeScript annotation strips nothing at runtime. Any authenticated caller could send a
 * `sealedSeed` key and replace the seed — no passphrase, no unlock, no confirmation — and since
 * encryptSecret happily encrypts nonsense, the damage only surfaced at the next unlock attempt. Keeping
 * the two apart means the next field added to that body cannot re-open it.
 */
export async function replaceSealedSeed(userId: number, id: number, sealedSeed: string): Promise<void> {
  await db
    .update(walletWallets)
    .set({ seedEnvelope: encryptSecret('wallet', sealedSeed), updatedAt: new Date() })
    .where(and(eq(walletWallets.userId, userId), eq(walletWallets.id, id)));
}

/** Exactly one active wallet per owner. Cleared and set in one transaction; the partial unique index is the backstop. */
export async function setActiveWallet(userId: number, id: number): Promise<void> {
  await db.transaction(async (tx) => {
    await tx
      .update(walletWallets)
      .set({ isActive: false })
      .where(and(eq(walletWallets.userId, userId), eq(walletWallets.isActive, true)));
    await tx
      .update(walletWallets)
      .set({ isActive: true, updatedAt: new Date() })
      .where(and(eq(walletWallets.userId, userId), eq(walletWallets.id, id)));
  });
}

export async function deleteWallet(userId: number, id: number): Promise<boolean> {
  const rows = await db
    .delete(walletWallets)
    .where(and(eq(walletWallets.userId, userId), eq(walletWallets.id, id)))
    .returning({ id: walletWallets.id });
  return rows.length > 0;
}

// ── labels ───────────────────────────────────────────────────────────────────────────────────────

export type WalletLabel = { kind: 'address' | 'tx'; ref: string; label: string };

export async function getWalletLabels(walletId: number): Promise<WalletLabel[]> {
  const rows = await db
    .select({ kind: walletLabels.kind, ref: walletLabels.ref, label: walletLabels.label })
    .from(walletLabels)
    .where(eq(walletLabels.walletId, walletId));
  return rows.map((r) => ({ kind: r.kind as 'address' | 'tx', ref: r.ref, label: r.label }));
}

export async function setWalletLabel(
  walletId: number,
  kind: 'address' | 'tx',
  ref: string,
  label: string,
): Promise<void> {
  // An empty label is a delete — the UI clears a field rather than pressing a separate button.
  if (!label.trim()) {
    await db
      .delete(walletLabels)
      .where(and(eq(walletLabels.walletId, walletId), eq(walletLabels.kind, kind), eq(walletLabels.ref, ref)));
    return;
  }
  await db
    .insert(walletLabels)
    .values({ walletId, kind, ref, label })
    .onConflictDoUpdate({ target: [walletLabels.walletId, walletLabels.kind, walletLabels.ref], set: { label } });
}

// ── frozen UTXOs ─────────────────────────────────────────────────────────────────────────────────

export async function getFrozenOutpoints(walletId: number): Promise<string[]> {
  const rows = await db
    .select({ outpoint: walletFrozenUtxos.outpoint })
    .from(walletFrozenUtxos)
    .where(eq(walletFrozenUtxos.walletId, walletId));
  return rows.map((r) => r.outpoint);
}

export async function setUtxoFrozen(
  walletId: number,
  outpoint: string,
  frozen: boolean,
  reason?: string,
): Promise<void> {
  if (!frozen) {
    await db
      .delete(walletFrozenUtxos)
      .where(and(eq(walletFrozenUtxos.walletId, walletId), eq(walletFrozenUtxos.outpoint, outpoint)));
    return;
  }
  await db
    .insert(walletFrozenUtxos)
    .values({ walletId, outpoint, reason: reason ?? null })
    .onConflictDoNothing();
}

// ── chain cache ──────────────────────────────────────────────────────────────────────────────────
//
// Unlike everything above, none of this is authoritative — see the comment on the table. These three
// functions are deliberately total: a cache read or write must never be able to fail a wallet request,
// so the sidecar treats every one of them as best-effort.

export type WalletChainCache = {
  snapshot: WalletChainSnapshot | null;
  syncedAt: Date | null;
  lastError: string | null;
  lastErrorAt: Date | null;
};

export async function getWalletChainCache(walletId: number): Promise<WalletChainCache | null> {
  const [row] = await db
    .select({
      snapshot: walletChainCache.snapshot,
      syncedAt: walletChainCache.syncedAt,
      lastError: walletChainCache.lastError,
      lastErrorAt: walletChainCache.lastErrorAt,
    })
    .from(walletChainCache)
    .where(eq(walletChainCache.walletId, walletId));
  return row ?? null;
}

/** A successful refresh. Clears the error columns — the upstream just answered, so nothing is wrong now. */
export async function saveWalletChainCache(walletId: number, snapshot: WalletChainSnapshot): Promise<void> {
  const syncedAt = new Date();
  await db
    .insert(walletChainCache)
    .values({ walletId, snapshot, syncedAt, lastError: null, lastErrorAt: null })
    .onConflictDoUpdate({
      target: walletChainCache.walletId,
      set: { snapshot, syncedAt, lastError: null, lastErrorAt: null },
    });
}

/** A failed refresh. Touches only the error columns, so the last good snapshot and its age survive. */
export async function recordWalletChainError(walletId: number, message: string): Promise<void> {
  const lastErrorAt = new Date();
  // Bounded because this is an upstream message verbatim, and an HTML error page from a misconfigured
  // proxy would otherwise land whole in the column.
  const lastError = message.slice(0, 500);
  await db
    .insert(walletChainCache)
    .values({ walletId, snapshot: null, syncedAt: null, lastError, lastErrorAt })
    .onConflictDoUpdate({ target: walletChainCache.walletId, set: { lastError, lastErrorAt } });
}
