import type {
  Balances,
  BackendKind,
  Capability,
  Channel,
  DecodedInvoice,
  FeeEstimates,
  Invoice,
  LockState,
  NodeInfo,
  OnchainTx,
  Payment,
  Peer,
  RescanState,
  SendCoinsResult,
  SyncState,
  Utxo,
  WalletConfig,
  WalletSummary,
} from './shared';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';
import { useClient } from 'hooks/useClient';

// Data layer for the /wallet panels. Everything talks to the officer-wallet sidecar through the
// /api/wallet auth proxy, which holds no key material of its own.
//
// THE WALLET IS USUALLY LOCKED, AND THAT IS THE NORMAL STATE. Every read below — balances, history,
// receive addresses, coin control, invoices — works against a locked wallet, because a locked wallet is
// still a watch-only wallet. Only signing needs the seed. So nothing here is gated on the lock, and no
// query is invalidated when the unlock window expires: the numbers on screen do not become wrong.
//
// PASSPHRASES ARE NEVER CACHED. The unlock/export/delete/change mutations take the passphrase as a call
// argument and hand it straight to the POST body. React Query stores mutation *variables* on the
// mutation object, so these are deliberately fire-and-forget: nothing here keeps a reference after the
// request resolves, and no passphrase is ever a query key or a query result.

const ROOT_KEY = ['wallet'] as const;

/** Balances move with the chain; a 20s poll is live enough without hammering Esplora. */
const BALANCE_POLL_MS = 20_000;
/** The lock countdown is rendered locally from `secondsRemaining`; this only re-syncs the truth. */
const LOCK_POLL_MS = 10_000;
/** History and coins are cheaper to refresh on demand than to poll hard. */
const HISTORY_POLL_MS = 60_000;
/** While a rescan runs, the balance response doubles as its progress feed — poll it like one. */
const RESCAN_POLL_MS = 4_000;

const EMPTY_WALLETS: WalletSummary[] = [];
const EMPTY_CAPS: Capability[] = [];
const EMPTY_TXS: OnchainTx[] = [];
const EMPTY_UTXOS: Utxo[] = [];
const EMPTY_INVOICES: Invoice[] = [];
const EMPTY_PAYMENTS: Payment[] = [];
const EMPTY_CHANNELS: Channel[] = [];
const EMPTY_PEERS: Peer[] = [];

const base = (id: number) => `/wallet/_officer/wallets/${id}`;

// ── deployment + wallet list ──────────────────────────────────────────────────────────────────────

export function useWalletConfig() {
  const { get } = useClient();

  const query = useQuery({
    queryKey: [...ROOT_KEY, 'config'] as const,
    queryFn: () => get<WalletConfig>('/wallet/_officer/config'),
    // Network and store-key config change only with a sidecar restart.
    staleTime: 5 * 60_000,
  });

  return { config: query.data ?? null, isLoading: query.isLoading, error: query.error };
}

/** The wallet list does not poll — it only changes when the owner creates, renames or deletes one. */
export function useWallets() {
  const { get } = useClient();

  const query = useQuery({
    queryKey: [...ROOT_KEY, 'wallets'] as const,
    queryFn: () => get<{ wallets: WalletSummary[] }>('/wallet/_officer/wallets'),
    staleTime: 30_000,
  });

  return {
    wallets: query.data?.wallets ?? EMPTY_WALLETS,
    isLoading: query.isLoading,
    error: query.error,
  };
}

// ── per-wallet reads ──────────────────────────────────────────────────────────────────────────────

/**
 * The lock state, polled. The countdown a user sees is interpolated locally in useLockCountdown — this
 * query is the periodic re-sync, not the clock.
 */
export function useLockState(walletId: number | null) {
  const { get } = useClient();

  const query = useQuery({
    queryKey: [...ROOT_KEY, 'lock-state', walletId] as const,
    queryFn: () => get<LockState>(`${base(walletId!)}/lock-state`),
    enabled: walletId != null,
    refetchInterval: LOCK_POLL_MS,
    staleTime: LOCK_POLL_MS - 1_000,
  });

  return { lock: query.data ?? null, dataUpdatedAt: query.dataUpdatedAt, isLoading: query.isLoading };
}

export function useCapabilities(walletId: number | null) {
  const { get } = useClient();

  const query = useQuery({
    queryKey: [...ROOT_KEY, 'capabilities', walletId] as const,
    queryFn: () => get<{ kind: BackendKind; capabilities: Capability[] }>(`${base(walletId!)}/capabilities`),
    enabled: walletId != null,
    // A backend's capability set is fixed for the life of the wallet.
    staleTime: 10 * 60_000,
  });

  return {
    kind: query.data?.kind ?? null,
    capabilities: query.data?.capabilities ?? EMPTY_CAPS,
    isLoading: query.isLoading,
    error: query.error,
  };
}

export function useWalletInfo(walletId: number | null) {
  const { get } = useClient();

  const query = useQuery({
    queryKey: [...ROOT_KEY, 'info', walletId] as const,
    queryFn: () => get<{ info: NodeInfo }>(`${base(walletId!)}/info`),
    enabled: walletId != null,
    refetchInterval: BALANCE_POLL_MS,
    staleTime: BALANCE_POLL_MS - 1_000,
  });

  return { info: query.data?.info ?? null, isLoading: query.isLoading, error: query.error };
}

// The three reads below carry a `sync` block from the sidecar. An on-chain wallet serves the last
// snapshot it managed to read and refreshes behind the response, so `isLoading` no longer means "we know
// nothing" — after the first successful read it is never true again, and the honest signal for "these
// numbers are old" or "we could not reach the chain" is `sync`, not the query state.

export function useBalances(walletId: number | null) {
  const { get } = useClient();

  const query = useQuery({
    queryKey: [...ROOT_KEY, 'balances', walletId] as const,
    queryFn: () => get<{ balances: Balances; sync: SyncState | null }>(`${base(walletId!)}/balances`),
    enabled: walletId != null,
    // Faster while the node is rescanning: this response carries the rescan's progress, and a counter
    // that moves once every twenty seconds reads as a hung one. Back to the ordinary poll when it ends.
    refetchInterval: (query) => (query.state.data?.sync?.rescan?.running ? RESCAN_POLL_MS : BALANCE_POLL_MS),
    staleTime: BALANCE_POLL_MS - 1_000,
  });

  return {
    balances: query.data?.balances ?? null,
    sync: query.data?.sync ?? null,
    isLoading: query.isLoading,
    error: query.error,
  };
}

export function useTransactions(walletId: number | null, limit = 50) {
  const { get } = useClient();

  const query = useQuery({
    queryKey: [...ROOT_KEY, 'transactions', walletId, limit] as const,
    queryFn: () =>
      get<{ transactions: OnchainTx[]; sync: SyncState | null }>(`${base(walletId!)}/transactions?limit=${limit}`),
    enabled: walletId != null,
    refetchInterval: HISTORY_POLL_MS,
    staleTime: 15_000,
  });

  return {
    transactions: query.data?.transactions ?? EMPTY_TXS,
    sync: query.data?.sync ?? null,
    isLoading: query.isLoading,
    error: query.error,
  };
}

export function useUtxos(walletId: number | null, enabled = true) {
  const { get } = useClient();

  const query = useQuery({
    queryKey: [...ROOT_KEY, 'utxos', walletId] as const,
    queryFn: () => get<{ utxos: Utxo[]; sync: SyncState | null }>(`${base(walletId!)}/utxos`),
    enabled: walletId != null && enabled,
    refetchInterval: HISTORY_POLL_MS,
    staleTime: 15_000,
  });

  return {
    utxos: query.data?.utxos ?? EMPTY_UTXOS,
    sync: query.data?.sync ?? null,
    isLoading: query.isLoading,
    error: query.error,
  };
}

export function useFees(walletId: number | null, enabled = true) {
  const { get } = useClient();

  const query = useQuery({
    queryKey: [...ROOT_KEY, 'fees', walletId] as const,
    queryFn: () => get<{ fees: FeeEstimates }>(`${base(walletId!)}/fees`),
    enabled: walletId != null && enabled,
    refetchInterval: 60_000,
    staleTime: 30_000,
  });

  return { fees: query.data?.fees ?? null, isLoading: query.isLoading, error: query.error };
}

/**
 * A receive address. `peek` returns the current unused address without advancing the derivation index —
 * which is what a screen that merely *displays* an address must do, or every render burns an address.
 */
type AddressResponse = { address: string; type: string };

export function useReceiveAddress(walletId: number | null, enabled = true) {
  const { get } = useClient();
  const qc = useQueryClient();
  const key = [...ROOT_KEY, 'address', walletId] as const;

  const query = useQuery({
    queryKey: key,
    queryFn: () => get<AddressResponse>(`${base(walletId!)}/address?peek=true`),
    enabled: walletId != null && enabled,
    staleTime: 60_000,
  });

  // `peek=false` CONSUMES: the sidecar advances its issued mark so this address is never handed out
  // again. Refetching the peeked query returns the same address forever, which is why this is a separate
  // call and not `refetch()` — that was the bug behind a "new address" button that changed nothing.
  const next = useMutation({
    mutationFn: () => get<AddressResponse>(`${base(walletId!)}/address?peek=false`),
    onSuccess: (data) => qc.setQueryData(key, data),
    onError: (err) => toast.error(errorMessage(err, 'Could not derive a new address')),
  });

  return {
    address: query.data?.address ?? null,
    addressType: query.data?.type ?? null,
    isLoading: query.isLoading,
    error: query.error,
    next,
  };
}

export function useInvoices(walletId: number | null, enabled = true, limit = 50) {
  const { get } = useClient();

  const query = useQuery({
    queryKey: [...ROOT_KEY, 'invoices', walletId, limit] as const,
    queryFn: () => get<{ invoices: Invoice[] }>(`${base(walletId!)}/invoices?limit=${limit}`),
    enabled: walletId != null && enabled,
    refetchInterval: 30_000,
    staleTime: 15_000,
  });

  return { invoices: query.data?.invoices ?? EMPTY_INVOICES, isLoading: query.isLoading, error: query.error };
}

export function usePayments(walletId: number | null, enabled = true, limit = 50) {
  const { get } = useClient();

  const query = useQuery({
    queryKey: [...ROOT_KEY, 'payments', walletId, limit] as const,
    queryFn: () => get<{ payments: Payment[] }>(`${base(walletId!)}/payments?limit=${limit}`),
    enabled: walletId != null && enabled,
    refetchInterval: 30_000,
    staleTime: 15_000,
  });

  return { payments: query.data?.payments ?? EMPTY_PAYMENTS, isLoading: query.isLoading, error: query.error };
}

export function useChannels(walletId: number | null, enabled = true) {
  const { get } = useClient();

  const query = useQuery({
    queryKey: [...ROOT_KEY, 'channels', walletId] as const,
    queryFn: () => get<{ channels: Channel[] }>(`${base(walletId!)}/channels`),
    enabled: walletId != null && enabled,
    refetchInterval: 30_000,
    staleTime: 15_000,
  });

  return { channels: query.data?.channels ?? EMPTY_CHANNELS, isLoading: query.isLoading, error: query.error };
}

export function usePeers(walletId: number | null, enabled = true) {
  const { get } = useClient();

  const query = useQuery({
    queryKey: [...ROOT_KEY, 'peers', walletId] as const,
    queryFn: () => get<{ peers: Peer[] }>(`${base(walletId!)}/peers`),
    enabled: walletId != null && enabled,
    refetchInterval: 30_000,
    staleTime: 15_000,
  });

  return { peers: query.data?.peers ?? EMPTY_PEERS, isLoading: query.isLoading, error: query.error };
}

// ── wallet lifecycle mutations ────────────────────────────────────────────────────────────────────

export type CreateWalletInput = {
  name: string;
  kind: BackendKind;
  network?: string;
  /** onchain only. Omit to have the sidecar generate one — that is the only case that returns a mnemonic. */
  mnemonic?: string;
  words?: 12 | 24;
  /** onchain only, and never stored anywhere on this side. */
  passphrase?: string;
  bip39Passphrase?: string;
  defaultBip?: number;
  config?: Record<string, unknown>;
  makeActive?: boolean;
};

export type CreateWalletResult = { wallet: WalletSummary; mnemonic?: string };

export function useWalletLifecycle() {
  const { post, patch, delete: del } = useClient();
  const qc = useQueryClient();
  const invalidateList = () => qc.invalidateQueries({ queryKey: [...ROOT_KEY, 'wallets'] });

  const create = useMutation({
    mutationFn: (input: CreateWalletInput) => post<CreateWalletResult>('/wallet/_officer/wallets', input),
    // Deliberately no onSuccess toast/cache write with the response: it may carry the mnemonic, and the
    // only place that is allowed to exist is the backup modal's local state. The caller invalidates.
    onSuccess: invalidateList,
    onError: (err) => toast.error(errorMessage(err, 'Could not create the wallet')),
  });

  const activate = useMutation({
    mutationFn: (walletId: number) => post<{ ok: true }>(`${base(walletId)}/activate`),
    onSuccess: () => {
      invalidateList();
      toast.success('Active wallet changed');
    },
    onError: (err) => toast.error(errorMessage(err, 'Could not activate the wallet')),
  });

  const rename = useMutation({
    // Only the label. The name is the one wallet field with no bearing on the key material, which is why
    // this needs neither the passphrase nor an unlocked wallet.
    mutationFn: ({ walletId, name }: { walletId: number; name: string }) =>
      patch<{ wallet: WalletSummary }>(`/wallet/_officer/wallets/${walletId}`, { name }),
    onSuccess: () => {
      invalidateList();
      toast.success('Wallet renamed');
    },
    onError: (err) => toast.error(errorMessage(err, 'Could not rename the wallet')),
  });

  const remove = useMutation({
    // The passphrase is required for a seeded wallet even when it is already unlocked — an open session
    // must not be enough to destroy the only copy of the key material.
    mutationFn: ({
      walletId,
      passphrase,
      confirmName,
    }: {
      walletId: number;
      passphrase?: string;
      confirmName: string;
    }) =>
      del<{ ok: true }>(`/wallet/_officer/wallets/${walletId}`, { confirmName, ...(passphrase ? { passphrase } : {}) }),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: ROOT_KEY });
      toast.success('Wallet deleted');
    },
    onError: (err) => toast.error(errorMessage(err, 'Could not delete the wallet')),
  });

  return { create, activate, rename, remove };
}

// ── lock lifecycle ────────────────────────────────────────────────────────────────────────────────

export function useLockActions(walletId: number | null) {
  const { post } = useClient();
  const qc = useQueryClient();
  const refreshLock = () => qc.invalidateQueries({ queryKey: [...ROOT_KEY, 'lock-state', walletId] });

  const unlock = useMutation({
    // `passphrase` goes from a controlled input straight into this body and the caller clears its state
    // immediately after. It is never persisted, cached or logged on this side.
    mutationFn: ({ passphrase, ttlSec }: { passphrase: string; ttlSec?: number }) =>
      post<{ ok: true; unlocked: boolean; secondsRemaining: number }>(`${base(walletId!)}/unlock`, {
        passphrase,
        ttlSec,
      }),
    onSuccess: () => {
      refreshLock();
      toast.success('Wallet unlocked');
    },
    onError: (err) => toast.error(errorMessage(err, 'Could not unlock — check the passphrase')),
  });

  const lock = useMutation({
    mutationFn: () => post<{ ok: true; unlocked: false }>(`${base(walletId!)}/lock`),
    onSuccess: () => {
      refreshLock();
      toast.success('Wallet locked');
    },
    onError: (err) => toast.error(errorMessage(err, 'Could not lock the wallet')),
  });

  const changePassphrase = useMutation({
    mutationFn: (input: { oldPassphrase: string; newPassphrase: string }) =>
      post<{ ok: true }>(`${base(walletId!)}/passphrase`, input),
    onSuccess: () => {
      refreshLock();
      toast.success('Passphrase changed — the wallet was re-locked');
    },
    onError: (err) => toast.error(errorMessage(err, 'Could not change the passphrase')),
  });

  const exportSeed = useMutation({
    mutationFn: (input: { passphrase: string }) =>
      post<{ mnemonic: string; hasBip39Passphrase: boolean }>(`${base(walletId!)}/export-seed`, input),
    // No cache write and no toast carrying the result — the mnemonic lives only in the dialog's state.
    onError: (err) => toast.error(errorMessage(err, 'Could not export the seed')),
  });

  return { unlock, lock, changePassphrase, exportSeed };
}

// ── operations ────────────────────────────────────────────────────────────────────────────────────

export type SendInput = {
  address: string;
  amountSats?: number;
  sendAll?: boolean;
  satPerVbyte: number;
  outpoints?: string[];
  label?: string;
};

export function useWalletOperations(walletId: number | null) {
  const { post } = useClient();
  const qc = useQueryClient();
  const invalidateAll = () => qc.invalidateQueries({ queryKey: ROOT_KEY });

  const send = useMutation({
    mutationFn: (input: SendInput) => post<SendCoinsResult>(`${base(walletId!)}/send`, input),
    onSuccess: (result) => {
      invalidateAll();
      toast.success(`Broadcast ${result.txid.slice(0, 12)}…`);
    },
    onError: (err) => toast.error(errorMessage(err, 'Could not send')),
  });

  const freeze = useMutation({
    mutationFn: (input: { outpoint: string; frozen: boolean; reason?: string }) =>
      post<{ ok: true }>(`${base(walletId!)}/utxos/freeze`, input),
    onSuccess: () => qc.invalidateQueries({ queryKey: [...ROOT_KEY, 'utxos', walletId] }),
    onError: (err) => toast.error(errorMessage(err, 'Could not change the freeze flag')),
  });

  const createInvoice = useMutation({
    mutationFn: (input: { amountMsat?: string; memo?: string; expirySeconds?: number; private?: boolean }) =>
      post<{ invoice: Invoice }>(`${base(walletId!)}/invoices`, input),
    onSuccess: () => qc.invalidateQueries({ queryKey: [...ROOT_KEY, 'invoices', walletId] }),
    onError: (err) => toast.error(errorMessage(err, 'Could not create the invoice')),
  });

  const decode = useMutation({
    mutationFn: (bolt11: string) => post<{ decoded: DecodedInvoice }>(`${base(walletId!)}/decode`, { bolt11 }),
    onError: (err) => toast.error(errorMessage(err, 'Could not decode that invoice')),
  });

  const pay = useMutation({
    mutationFn: (input: { bolt11: string; amountMsat?: string; feeLimitMsat?: string }) =>
      post<{ payment: Payment }>(`${base(walletId!)}/pay`, input),
    onSuccess: (data) => {
      invalidateAll();
      if (data.payment.status === 'failed') toast.error(data.payment.failureReason ?? 'Payment failed');
      else toast.success(data.payment.status === 'succeeded' ? 'Payment sent' : 'Payment in flight');
    },
    onError: (err) => toast.error(errorMessage(err, 'Could not pay')),
  });

  const keysend = useMutation({
    mutationFn: (input: { destination: string; amountMsat: string; message?: string }) =>
      post<{ payment: Payment }>(`${base(walletId!)}/keysend`, input),
    onSuccess: () => {
      invalidateAll();
      toast.success('Keysend sent');
    },
    onError: (err) => toast.error(errorMessage(err, 'Keysend failed')),
  });

  const sign = useMutation({
    mutationFn: (message: string) => post<{ signature: string }>(`${base(walletId!)}/sign`, { message }),
    onError: (err) => toast.error(errorMessage(err, 'Could not sign — the wallet may be locked')),
  });

  const verify = useMutation({
    mutationFn: (input: { message: string; signature: string }) =>
      post<{ valid: boolean; pubkey: string | null }>(`${base(walletId!)}/verify`, input),
    onError: (err) => toast.error(errorMessage(err, 'Could not verify')),
  });

  const setLabel = useMutation({
    mutationFn: (input: { kind: 'address' | 'tx'; ref: string; label: string }) =>
      post<{ ok: true }>(`${base(walletId!)}/labels`, input),
    onSuccess: () => invalidateAll(),
    onError: (err) => toast.error(errorMessage(err, 'Could not save the label')),
  });

  // Returns as soon as the scan is queued — it takes minutes. Progress arrives on the `sync.rescan` block
  // of the balances poll, which speeds up on its own while one is running, so the invalidation here is
  // only to put the first "scanning…" on screen without waiting out the current interval.
  const rescan = useMutation({
    mutationFn: (input: { gapLimit?: number } = {}) =>
      post<{ rescan: RescanState }>(`${base(walletId!)}/rescan`, input),
    onSuccess: () => {
      qc.invalidateQueries({ queryKey: [...ROOT_KEY, 'balances', walletId] });
      toast.success('Scanning the chain for this wallet’s coins — this takes a few minutes');
    },
    onError: (err) => toast.error(errorMessage(err, 'Could not start the scan')),
  });

  return { send, freeze, createInvoice, decode, pay, keysend, sign, verify, setLabel, rescan };
}

/**
 * useClient rejects with `{status, message}` rather than an Error, and the sidecar's message is a JSON
 * body — unwrap both so a toast reads "this wallet does not support coin control" rather than
 * "[object Object]". Never called with anything that could contain a passphrase: the sidecar's error
 * bodies are messages and codes only.
 */
export function errorMessage(err: unknown, fallback: string): string {
  const raw = typeof err === 'object' && err !== null && 'message' in err ? String(err.message) : '';
  if (!raw) return fallback;
  try {
    const parsed = JSON.parse(raw) as { error?: string };
    if (parsed.error) return parsed.error;
  } catch {
    /* not JSON — use it as-is */
  }
  return raw || fallback;
}
