"use client";

import Link from "next/link";
import { useCallback, useEffect, useRef, useState } from "react";
import { AppShell } from "@/components/AppShell";
import { ContextReports } from "@/components/ContextReports";
import {
  createBankMovement,
  getBankFacets,
  getBankStats,
  getBankSummary,
  listBankAccounts,
  listBankMovements,
  voidBankMovement,
} from "@/lib/api";
import { useCan } from "@/lib/abilities";
import {
  bankDirectionLabel,
  bankSourceLabel,
  bankTone,
  formatDate,
  formatMoney,
  formatNumber,
  monthName,
} from "@/lib/labels";
import type {
  BankAccount,
  BankCleared,
  BankDirection,
  BankFacets,
  BankListItem,
  BankListResponse,
  BankSort,
  BankStats,
  BankSummary,
  BankTotals,
  CreateBankMovementInput,
  Currency,
} from "@/lib/types";

/**
 * Bank register (chequera) browser — plan step 7, multi-account since the
 * step-11 multi-bank work.
 *
 * This is the office's OWN checking accounts, not customer money. It is a
 * separate page from /estado-cuenta on purpose: nothing here belongs in a
 * customer's statement and the two sets of figures are never combined.
 *
 * Two views, both scoped to the ONE account picked at the top:
 *   - "Movimientos": the register itself — every deposit and payment, by date,
 *     payee, cheque number or amount.
 *   - "Resumen": ingresos vs egresos per year, and per month inside a year,
 *     with the running net movement since the register opened.
 *
 * Every amount is read in the selected account's currency. There is no "all
 * accounts" option on purpose — Utilities banks in MXN and Seguros in USD, so
 * one combined figure would be a number that never existed, exactly what
 * /estado-cuenta's per-currency rule avoids. See the module header in
 * `bank.service.ts` for why there is no category/ramo filter.
 */
type View = "movimientos" | "resumen";

const DIRECTIONS: { key: BankDirection | ""; label: string }[] = [
  { key: "", label: "Ingresos y egresos" },
  { key: "income", label: "Sólo ingresos" },
  { key: "expense", label: "Sólo egresos" },
  { key: "void", label: "Sólo cancelados" },
];

const CLEARED: { key: BankCleared | ""; label: string }[] = [
  { key: "", label: "Operados y pendientes" },
  { key: "cleared", label: "Sólo operados" },
  { key: "pending", label: "Sólo pendientes" },
];

const SORTS: { key: BankSort; label: string }[] = [
  { key: "date_desc", label: "Fecha (más reciente)" },
  { key: "date_asc", label: "Fecha (más antigua)" },
  { key: "amount_desc", label: "Ingreso más grande" },
  { key: "amount_asc", label: "Egreso más grande" },
  { key: "reference", label: "Número de cheque" },
];

export default function BancoPage() {
  return (
    <AppShell>
      <BankBrowser />
    </AppShell>
  );
}

/** Remembers the last chequera a person looked at, per browser. */
const ACCOUNT_KEY = "banco.bankAccountId";

function BankBrowser() {
  const canCapture = useCan("bank:create");
  const canVoid = useCan("bank:void");
  const canManageAccounts = useCan("bank:manage-accounts");

  const [accounts, setAccounts] = useState<BankAccount[] | null>(null);
  const [accountId, setAccountId] = useState<string | null>(null);
  const [accountsError, setAccountsError] = useState<string | null>(null);

  const [stats, setStats] = useState<BankStats | null>(null);
  const [facets, setFacets] = useState<BankFacets | null>(null);
  const [view, setView] = useState<View>("movimientos");

  const [query, setQuery] = useState("");
  const [direction, setDirection] = useState<BankDirection | "">("");
  const [cleared, setCleared] = useState<BankCleared | "">("");
  const [from, setFrom] = useState("");
  const [to, setTo] = useState("");
  const [sort, setSort] = useState<BankSort>("date_desc");

  const [movements, setMovements] = useState<BankListResponse | null>(null);
  const [summary, setSummary] = useState<BankSummary | null>(null);
  const [summaryYear, setSummaryYear] = useState<number | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [captureOpen, setCaptureOpen] = useState(false);

  const debounceRef = useRef<ReturnType<typeof setTimeout>>();

  const account = accounts?.find((a) => a.id === accountId) ?? null;
  const currency = account?.currency ?? "MXN";

  // Accounts load first: nothing else on this page can be requested until one
  // is selected, because every read is scoped to exactly one chequera.
  useEffect(() => {
    listBankAccounts()
      .then((rows) => {
        setAccounts(rows);
        const remembered =
          typeof window !== "undefined"
            ? window.localStorage.getItem(ACCOUNT_KEY)
            : null;
        const pick =
          rows.find((a) => a.id === remembered) ??
          rows.find((a) => a.active) ??
          rows[0];
        setAccountId(pick?.id ?? null);
        if (rows.length === 0) setLoading(false);
      })
      .catch((e) => {
        setAccountsError(e?.message ?? "No se pudieron cargar las cuentas.");
        setLoading(false);
      });
  }, []);

  function pickAccount(id: string) {
    setAccountId(id);
    if (typeof window !== "undefined")
      window.localStorage.setItem(ACCOUNT_KEY, id);
    // The previous account's figures must not linger while the new ones load.
    setStats(null);
    setFacets(null);
    setMovements(null);
    setSummary(null);
    setSummaryYear(null);
  }

  const refreshStats = useCallback(() => {
    if (!accountId) return;
    getBankStats(accountId)
      .then(setStats)
      .catch(() => setStats(null));
  }, [accountId]);

  useEffect(() => {
    if (!accountId) return;
    refreshStats();
    getBankFacets(accountId)
      .then(setFacets)
      .catch(() => setFacets(null));
  }, [accountId, refreshStats]);

  const runSearch = useCallback(
    (p: number) => {
      if (!accountId) return;
      setLoading(true);
      setError(null);
      listBankMovements({
        bankAccountId: accountId,
        query: query || undefined,
        direction: direction || undefined,
        cleared: cleared || undefined,
        from: from || undefined,
        to: to || undefined,
        sort,
        page: p,
        pageSize: 25,
      })
        .then((res) => {
          setMovements(res);
          setLoading(false);
        })
        .catch((e) => {
          setError(e?.message ?? "No se pudieron cargar los movimientos.");
          setLoading(false);
        });
    },
    [accountId, query, direction, cleared, from, to, sort],
  );

  useEffect(() => {
    if (view !== "movimientos" || !accountId) return;
    if (debounceRef.current) clearTimeout(debounceRef.current);
    debounceRef.current = setTimeout(() => runSearch(1), 280);
    return () => {
      if (debounceRef.current) clearTimeout(debounceRef.current);
    };
  }, [runSearch, view, accountId]);

  useEffect(() => {
    if (view !== "resumen" || !accountId) return;
    setLoading(true);
    setError(null);
    getBankSummary(accountId, summaryYear ?? undefined)
      .then((res) => {
        setSummary(res);
        setLoading(false);
      })
      .catch((e) => {
        setError(e?.message ?? "No se pudo cargar el resumen.");
        setLoading(false);
      });
  }, [view, summaryYear, accountId]);

  function goToPage(p: number) {
    runSearch(p);
    if (typeof window !== "undefined")
      window.scrollTo({ top: 0, behavior: "smooth" });
  }

  /** Clicking a year in the resumen drills into its months. */
  function pickYear(year: number) {
    setSummaryYear((prev) => (prev === year ? null : year));
  }

  /** Jumping from a headline figure lands on the matching filtered register. */
  function pickDirection(d: BankDirection) {
    setView("movimientos");
    setDirection(d);
    setSort(d === "income" ? "amount_desc" : "amount_asc");
  }

  const filtered =
    query !== "" ||
    direction !== "" ||
    cleared !== "" ||
    from !== "" ||
    to !== "" ||
    sort !== "date_desc";

  function clearFilters() {
    setQuery("");
    setDirection("");
    setCleared("");
    setFrom("");
    setTo("");
    setSort("date_desc");
  }

  if (accountsError) {
    return (
      <>
        <div className="page-head">
          <h1 className="page-title">Chequera</h1>
        </div>
        <div className="state-error" role="alert">
          {accountsError}
        </div>
      </>
    );
  }

  // No chequera on file: the register has nothing it could be scoped to.
  if (accounts && accounts.length === 0) {
    return (
      <>
        <div className="page-head">
          <p className="eyebrow">Cuentas propias de la oficina</p>
          <h1 className="page-title">Chequera</h1>
        </div>
        <div className="state-box">
          <div className="state-glyph" aria-hidden>
            ⌗
          </div>
          <h3>Sin cuentas registradas</h3>
          <p>
            {canManageAccounts ? (
              <>
                Registra una cuenta bancaria en{" "}
                <Link href="/banco/cuentas">Cuentas de chequera</Link> para
                empezar a capturar movimientos.
              </>
            ) : (
              "Pide a un administrador que registre una cuenta bancaria."
            )}
          </p>
        </div>
      </>
    );
  }

  return (
    <>
      <div className="page-head rise">
        <p className="eyebrow">Cuenta propia de la oficina</p>
        <h1 className="page-title">Chequera</h1>
        <AccountPicker
          accounts={accounts}
          accountId={accountId}
          onPick={pickAccount}
          canManageAccounts={canManageAccounts}
        />
        <BankStatStrip
          stats={stats}
          currency={currency}
          direction={view === "movimientos" ? direction : ""}
          onPickDirection={pickDirection}
        />
        <p className="section-note">
          Movimientos de{" "}
          <strong>{account ? account.label : "la cuenta seleccionada"}</strong>,
          en {currency}. Cada cuenta se lee por separado: las cifras de dos
          chequeras nunca se suman, igual que los saldos por moneda del estado
          de cuenta. Tampoco forman parte del estado de cuenta de los clientes.
        </p>
        <div style={{ marginTop: 8 }}>
          <ContextReports
            entries={[
              { slug: "reporte-de-efectivo", label: "Reporte de efectivo" },
            ]}
          />
        </div>
      </div>

      <div className="toolbar">
        <div className="search-box">
          <span className="search-icon" aria-hidden>
            ⌕
          </span>
          <input
            className="input search-input"
            type="search"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder="Buscar por beneficiario, cheque, nota…"
            aria-label="Buscar en la chequera"
            disabled={view === "resumen"}
          />
        </div>
        <div className="seg" role="tablist" aria-label="Vista">
          {(
            [
              { key: "movimientos" as View, label: "Movimientos" },
              { key: "resumen" as View, label: "Resumen por periodo" },
            ]
          ).map((v) => (
            <button
              key={v.key}
              type="button"
              role="tab"
              aria-selected={view === v.key}
              className={`seg-btn ${view === v.key ? "active" : ""}`}
              onClick={() => setView(v.key)}
            >
              {v.label}
            </button>
          ))}
        </div>
        {view === "movimientos" && canCapture && account?.active && (
          <button
            type="button"
            className="btn btn-primary"
            onClick={() => setCaptureOpen((v) => !v)}
          >
            {captureOpen ? "Cerrar captura" : "Capturar movimiento"}
          </button>
        )}
      </div>

      {account && !account.active && (
        <div className="section-note">
          Esta cuenta está cerrada: su historial se consulta, pero no admite
          movimientos nuevos.
        </div>
      )}

      {view === "movimientos" && captureOpen && account && (
        <BankCaptureForm
          account={account}
          onSaved={() => {
            setCaptureOpen(false);
            runSearch(movements?.page ?? 1);
            refreshStats();
          }}
          onCancel={() => setCaptureOpen(false)}
        />
      )}

      {view === "movimientos" && (
        <div className="filter-row">
          <label className="filter-field">
            <span className="filter-label">Movimiento</span>
            <select
              className="input select"
              value={direction}
              onChange={(e) =>
                setDirection(e.target.value as BankDirection | "")
              }
            >
              {DIRECTIONS.map((d) => (
                <option key={d.key} value={d.key}>
                  {d.label}
                </option>
              ))}
            </select>
          </label>

          <label className="filter-field">
            <span className="filter-label">Estatus</span>
            <select
              className="input select"
              value={cleared}
              onChange={(e) => setCleared(e.target.value as BankCleared | "")}
            >
              {CLEARED.map((c) => (
                <option key={c.key} value={c.key}>
                  {c.label}
                </option>
              ))}
            </select>
          </label>

          <label className="filter-field">
            <span className="filter-label">Desde</span>
            <input
              className="input"
              type="date"
              value={from}
              onChange={(e) => setFrom(e.target.value)}
            />
          </label>

          <label className="filter-field">
            <span className="filter-label">Hasta</span>
            <input
              className="input"
              type="date"
              value={to}
              onChange={(e) => setTo(e.target.value)}
            />
          </label>

          <label className="filter-field">
            <span className="filter-label">Ordenar por</span>
            <select
              className="input select"
              value={sort}
              onChange={(e) => setSort(e.target.value as BankSort)}
            >
              {SORTS.map((s) => (
                <option key={s.key} value={s.key}>
                  {s.label}
                </option>
              ))}
            </select>
          </label>

          {filtered && (
            <button
              type="button"
              className="btn btn-ghost filter-clear"
              onClick={clearFilters}
            >
              Limpiar filtros
            </button>
          )}
        </div>
      )}

      {view === "resumen" && facets && facets.years.length > 0 && (
        <div className="filter-row">
          <label className="filter-field">
            <span className="filter-label">Año</span>
            <select
              className="input select"
              value={summaryYear ?? ""}
              onChange={(e) =>
                setSummaryYear(e.target.value ? Number(e.target.value) : null)
              }
            >
              <option value="">Todos los años</option>
              {facets.years.map((y) => (
                <option key={y.year} value={y.year}>
                  {y.year} ({formatNumber(y.count)})
                </option>
              ))}
            </select>
          </label>
        </div>
      )}

      {view === "movimientos" && movements && !loading && !error && (
        <div className="result-meta" aria-live="polite">
          {movements.total === 0
            ? "Sin resultados"
            : `${formatNumber(movements.total)} ${
                movements.total === 1 ? "movimiento" : "movimientos"
              }`}
          {query ? ` para “${query}”` : ""}
        </div>
      )}

      {view === "movimientos" && movements && !loading && (
        <FilteredTotals totals={movements.totals} currency={currency} />
      )}

      {error ? (
        <div className="state-error" role="alert">
          {error}
        </div>
      ) : loading ? (
        <ListSkeleton />
      ) : view === "resumen" ? (
        <SummaryView
          summary={summary}
          year={summaryYear}
          currency={currency}
          onPickYear={pickYear}
        />
      ) : movements && movements.total === 0 ? (
        <EmptyState query={query} />
      ) : (
        <>
          <div className="card">
            <div className="tx-scroll">
              <table className="tx-table">
                <thead>
                  <tr>
                    <th>Fecha</th>
                    <th>Cheque / ref.</th>
                    <th>Beneficiario / concepto</th>
                    <th>Origen</th>
                    <th className="num">Monto</th>
                    {canVoid && (
                      <th style={{ width: 1, whiteSpace: "nowrap" }}>
                        Acciones
                      </th>
                    )}
                  </tr>
                </thead>
                <tbody>
                  {movements?.items.map((m) => (
                    <BankRow
                      key={m.id}
                      m={m}
                      currency={currency}
                      canVoid={canVoid}
                      onVoided={() => {
                        runSearch(movements?.page ?? 1);
                        refreshStats();
                      }}
                    />
                  ))}
                </tbody>
              </table>
            </div>
          </div>
          {movements && movements.pageCount > 1 && (
            <Pager
              page={movements.page}
              pageCount={movements.pageCount}
              onChange={goToPage}
            />
          )}
        </>
      )}
    </>
  );
}

/**
 * Which chequera the whole page is reading. There is no "todas las cuentas"
 * option and there must not be one — see the file header.
 */
function AccountPicker({
  accounts,
  accountId,
  onPick,
  canManageAccounts,
}: {
  accounts: BankAccount[] | null;
  accountId: string | null;
  onPick: (id: string) => void;
  canManageAccounts: boolean;
}) {
  if (!accounts) {
    return (
      <div className="skeleton" style={{ height: 34, width: 260, marginTop: 12 }} />
    );
  }

  return (
    <div
      className="filter-row"
      style={{ marginTop: 12, alignItems: "flex-end" }}
    >
      <label className="filter-field">
        <span className="filter-label">Cuenta</span>
        <select
          className="input select"
          value={accountId ?? ""}
          onChange={(e) => onPick(e.target.value)}
          aria-label="Cuenta de chequera"
        >
          {accounts.map((a) => (
            <option key={a.id} value={a.id}>
              {a.label} · {a.currency}
              {a.active ? "" : " (cerrada)"}
            </option>
          ))}
        </select>
      </label>
      {canManageAccounts && (
        <Link href="/banco/cuentas" className="btn btn-ghost">
          Administrar cuentas
        </Link>
      )}
    </div>
  );
}

/** Headline figures; the ingreso/egreso cells double as register shortcuts. */
function BankStatStrip({
  stats,
  currency,
  direction,
  onPickDirection,
}: {
  stats: BankStats | null;
  currency: Currency;
  direction: BankDirection | "";
  onPickDirection: (d: BankDirection) => void;
}) {
  if (!stats) {
    return (
      <div className="stat-strip" aria-hidden>
        {Array.from({ length: 5 }).map((_, i) => (
          <div className="stat-cell" key={i}>
            <div className="skeleton" style={{ height: 25, width: "60%" }} />
            <div
              className="skeleton"
              style={{ height: 11, width: "80%", marginTop: 8 }}
            />
          </div>
        ))}
      </div>
    );
  }

  return (
    <div className="stat-strip">
      <button
        type="button"
        className={`stat-cell stat-cell-btn accent${
          direction === "income" ? " selected" : ""
        }`}
        onClick={() => onPickDirection("income")}
        aria-pressed={direction === "income"}
      >
        <div className="stat-value tx-amount pos">
          {formatMoney(stats.income, currency)}
        </div>
        <div className="stat-label">
          En ingresos · {formatNumber(stats.incomeCount)} movimientos
        </div>
      </button>
      <button
        type="button"
        className={`stat-cell stat-cell-btn${
          direction === "expense" ? " selected" : ""
        }`}
        onClick={() => onPickDirection("expense")}
        aria-pressed={direction === "expense"}
      >
        <div className="stat-value tx-amount neg">
          {formatMoney(stats.expense, currency)}
        </div>
        <div className="stat-label">
          En egresos · {formatNumber(stats.expenseCount)} movimientos
        </div>
      </button>
      <div className="stat-cell">
        <div className="stat-value">{formatMoney(stats.net, currency)}</div>
        {/* Not the bank balance: the register carries no opening balance. */}
        <div className="stat-label">Movimiento neto acumulado</div>
      </div>
      <div className="stat-cell">
        <div className="stat-value">{formatNumber(stats.movements)}</div>
        <div className="stat-label">
          Movimientos · {formatDate(stats.firstMovement)} a{" "}
          {formatDate(stats.lastMovement)}
        </div>
      </div>
      <button
        type="button"
        className={`stat-cell stat-cell-btn${
          direction === "void" ? " selected" : ""
        }`}
        onClick={() => onPickDirection("void")}
        aria-pressed={direction === "void"}
      >
        <div className="stat-value">{formatNumber(stats.voidCount)}</div>
        <div className="stat-label">
          Cheques cancelados · {formatNumber(stats.pending)} sin operar
        </div>
      </button>
    </div>
  );
}

/** Totals for everything the current filter matched, not just the page. */
function FilteredTotals({
  totals,
  currency,
}: {
  totals: BankTotals;
  currency: Currency;
}) {
  if (totals.incomeCount + totals.expenseCount + totals.voidCount === 0)
    return null;
  return (
    <div className="filtered-totals">
      <div className="filtered-total">
        <span className="filtered-total-cur">{currency}</span>
        <span>
          <strong className="tx-amount pos">
            {formatMoney(totals.income, currency)}
          </strong>{" "}
          en ingresos · {formatNumber(totals.incomeCount)}
        </span>
        <span>
          <strong className="tx-amount neg">
            {formatMoney(totals.expense, currency)}
          </strong>{" "}
          en egresos · {formatNumber(totals.expenseCount)}
        </span>
        <span className="filtered-total-net">
          Neto <strong>{formatMoney(totals.net, currency)}</strong>
        </span>
        {totals.voidCount > 0 && (
          <span>{formatNumber(totals.voidCount)} cancelados</span>
        )}
      </div>
    </div>
  );
}

function BankRow({
  m,
  currency,
  canVoid,
  onVoided,
}: {
  m: BankListItem;
  currency: Currency;
  canVoid: boolean;
  onVoided: () => void;
}) {
  const [busy, setBusy] = useState(false);

  async function doVoid() {
    if (
      !window.confirm(
        "¿Anular este movimiento? Quedará tachado y no contará en los totales.",
      )
    )
      return;
    setBusy(true);
    try {
      await voidBankMovement(m.id);
      onVoided();
    } catch (e) {
      window.alert(
        (e as Error)?.message ?? "No se pudo anular el movimiento.",
      );
      setBusy(false);
    }
  }

  return (
    <tr style={m.voided ? { textDecoration: "line-through", opacity: 0.55 } : undefined}>
      <td className="mono" style={{ whiteSpace: "nowrap" }}>
        {formatDate(m.transactionDate)}
      </td>
      <td className="tx-ref">
        {m.reference || "—"}
        {!m.cleared && <div className="tx-concept">Sin operar</div>}
      </td>
      <td>
        {m.concept || <span className="muted">Sin concepto</span>}
        {m.notes && <div className="tx-concept">Nota: {m.notes}</div>}
      </td>
      <td>{bankSourceLabel(m.source)}</td>
      <td className="num">
        <span className={`tx-amount ${bankTone(m.direction)}`}>
          {m.direction === "void" ? "—" : formatMoney(m.amount, currency)}
        </span>
        <div className="tx-cur">{bankDirectionLabel(m.direction)}</div>
      </td>
      {canVoid && (
        <td style={{ whiteSpace: "nowrap" }}>
          {!m.voided && (
            <button
              type="button"
              className="btn btn-ghost"
              style={{ padding: "4px 10px", fontSize: 12 }}
              onClick={doVoid}
              disabled={busy}
            >
              {busy ? "Anulando…" : "Anular"}
            </button>
          )}
        </td>
      )}
    </tr>
  );
}

/**
 * Ingresos vs egresos per period. `cumulative` is the net movement since the
 * register opened in 2013, not a bank balance — SCOTHIA has no opening figure.
 */
function SummaryView({
  summary,
  year,
  currency,
  onPickYear,
}: {
  summary: BankSummary | null;
  year: number | null;
  currency: Currency;
  onPickYear: (y: number) => void;
}) {
  if (!summary) return null;

  return (
    <>
      <div className="card">
        <div className="tx-scroll">
          <table className="tx-table">
            <thead>
              <tr>
                <th>Año</th>
                <th className="num">Movimientos</th>
                <th className="num">Ingresos</th>
                <th className="num">Egresos</th>
                <th className="num">Neto</th>
                <th className="num">Acumulado</th>
              </tr>
            </thead>
            <tbody>
              {summary.years.map((r) => (
                <tr
                  key={r.period}
                  onClick={() => onPickYear(r.period)}
                  style={{ cursor: "pointer" }}
                  className={year === r.period ? "selected" : undefined}
                >
                  <td className="mono">
                    <strong>{r.period}</strong>
                  </td>
                  <td className="num">{formatNumber(r.count)}</td>
                  <td className="num">
                    <span className="tx-amount pos">
                      {formatMoney(r.income, currency)}
                    </span>
                  </td>
                  <td className="num">
                    <span className="tx-amount neg">
                      {formatMoney(r.expense, currency)}
                    </span>
                  </td>
                  <td className="num">
                    <span
                      className={`tx-amount ${
                        Number(r.net) < 0 ? "neg" : "pos"
                      }`}
                    >
                      {formatMoney(r.net, currency)}
                    </span>
                  </td>
                  <td className="num mono">{formatMoney(r.cumulative, currency)}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      </div>

      <p className="section-note">
        El acumulado es el movimiento neto desde que inicia el registro; la
        chequera heredada no trae saldo inicial, así que no equivale al saldo
        del banco. Selecciona un año para ver sus meses.
      </p>

      {year && summary.months.length > 0 && (
        <div className="section">
          <div className="section-head">
            <span className="section-rule cuenta" />
            <h2 className="section-title">Meses de {year}</h2>
            <span className="section-count">
              abre en {formatMoney(summary.opening, currency)}
            </span>
          </div>
          <div className="card">
            <div className="tx-scroll">
              <table className="tx-table">
                <thead>
                  <tr>
                    <th>Mes</th>
                    <th className="num">Movimientos</th>
                    <th className="num">Ingresos</th>
                    <th className="num">Egresos</th>
                    <th className="num">Neto</th>
                    <th className="num">Acumulado</th>
                  </tr>
                </thead>
                <tbody>
                  {summary.months.map((r) => (
                    <tr key={r.period}>
                      <td>{monthName(r.period)}</td>
                      <td className="num">{formatNumber(r.count)}</td>
                      <td className="num">
                        <span className="tx-amount pos">
                          {formatMoney(r.income, currency)}
                        </span>
                      </td>
                      <td className="num">
                        <span className="tx-amount neg">
                          {formatMoney(r.expense, currency)}
                        </span>
                      </td>
                      <td className="num">
                        <span
                          className={`tx-amount ${
                            Number(r.net) < 0 ? "neg" : "pos"
                          }`}
                        >
                          {formatMoney(r.net, currency)}
                        </span>
                      </td>
                      <td className="num mono">
                        {formatMoney(r.cumulative, currency)}
                      </td>
                    </tr>
                  ))}
                </tbody>
              </table>
            </div>
          </div>
        </div>
      )}
    </>
  );
}

/** Inline capture form for a single chequera movement. The amount is in the
 *  selected account's currency; sign convention: positive = ingreso, negative
 *  = egreso. Booked rows are never edited — fix mistakes with voidBankMovement
 *  + a fresh capture. */
function BankCaptureForm({
  account,
  onSaved,
  onCancel,
}: {
  account: BankAccount;
  onSaved: () => void;
  onCancel: () => void;
}) {
  const [direction, setDirection] = useState<BankDirection>("expense");
  const [amount, setAmount] = useState("");
  const [transactionDate, setTransactionDate] = useState(
    new Date().toISOString().slice(0, 10),
  );
  const [concept, setConcept] = useState("");
  const [reference, setReference] = useState("");
  const [transactionType, setTransactionType] = useState("");
  const [cleared, setCleared] = useState(true);
  const [transferred, setTransferred] = useState(false);
  const [notes, setNotes] = useState("");
  const [amountInWords, setAmountInWords] = useState("");
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState<string | null>(null);

  function s(v: string): string | undefined {
    const t = v.trim();
    return t === "" ? undefined : t;
  }

  async function submit(e: React.FormEvent) {
    e.preventDefault();
    const abs = Number(amount);
    if (!Number.isFinite(abs) || abs <= 0) {
      setError("El monto debe ser un número mayor a cero.");
      return;
    }
    const signed = direction === "income" ? Math.abs(abs) : -Math.abs(abs);
    const payload: CreateBankMovementInput = {
      bankAccountId: account.id,
      amount: signed,
      transactionDate,
      concept: s(concept),
      reference: s(reference),
      transactionType: s(transactionType),
      cleared,
      transferred,
      notes: s(notes),
      amountInWords: s(amountInWords),
    };
    setSaving(true);
    setError(null);
    try {
      await createBankMovement(payload);
      onSaved();
    } catch (e2) {
      setError((e2 as Error)?.message ?? "No se pudo guardar el movimiento.");
      setSaving(false);
    }
  }

  return (
    <form onSubmit={submit}>
      {error && <div className="state-box state-error">{error}</div>}
      <div className="card" style={{ padding: 20, marginBottom: 16 }}>
        <h2 className="section-title" style={{ marginBottom: 4 }}>
          Capturar movimiento de chequera
        </h2>
        <p className="section-note" style={{ marginBottom: 14 }}>
          Se registra en <strong>{account.label}</strong>, en {account.currency}.
        </p>
        <div className="form-grid">
          <label className="field">
            <span className="field-label">
              Tipo <span aria-hidden>*</span>
            </span>
            <select
              className="select"
              value={direction}
              onChange={(e) => setDirection(e.target.value as BankDirection)}
            >
              <option value="income">Ingreso</option>
              <option value="expense">Egreso</option>
            </select>
          </label>
          <label className="field">
            <span className="field-label">
              Fecha <span aria-hidden>*</span>
            </span>
            <input
              className="input"
              type="date"
              required
              value={transactionDate}
              onChange={(e) => setTransactionDate(e.target.value)}
            />
          </label>
          <label className="field">
            <span className="field-label">
              Monto ({account.currency}) <span aria-hidden>*</span>
            </span>
            <input
              className="input"
              type="number"
              step="0.01"
              required
              min="0"
              value={amount}
              onChange={(e) => setAmount(e.target.value)}
              placeholder="0.00"
            />
          </label>
          <label className="field">
            <span className="field-label">Concepto</span>
            <input
              className="input"
              value={concept}
              onChange={(e) => setConcept(e.target.value)}
              placeholder="Beneficiario o motivo"
            />
          </label>
          <label className="field">
            <span className="field-label">Referencia / cheque</span>
            <input
              className="input"
              value={reference}
              onChange={(e) => setReference(e.target.value)}
            />
          </label>
          <label className="field">
            <span className="field-label">Tipo en origen</span>
            <input
              className="input"
              value={transactionType}
              onChange={(e) => setTransactionType(e.target.value)}
              placeholder="INGRESO / EGRESO"
            />
          </label>
          <label className="field">
            <span className="field-label">Monto en letras</span>
            <input
              className="input"
              value={amountInWords}
              onChange={(e) => setAmountInWords(e.target.value)}
              placeholder="Ej. CIENTO CINCUENTA MIL PESOS 00/100"
            />
          </label>
          <label
            className="field"
            style={{ flexDirection: "row", alignItems: "center", gap: 8 }}
          >
            <input
              type="checkbox"
              checked={cleared}
              onChange={(e) => setCleared(e.target.checked)}
            />
            <span className="field-label" style={{ margin: 0 }}>
              Operado por el banco
            </span>
          </label>
          <label
            className="field"
            style={{ flexDirection: "row", alignItems: "center", gap: 8 }}
          >
            <input
              type="checkbox"
              checked={transferred}
              onChange={(e) => setTransferred(e.target.checked)}
            />
            <span className="field-label" style={{ margin: 0 }}>
              Transferencia
            </span>
          </label>
        </div>
        <label className="field" style={{ marginTop: 16 }}>
          <span className="field-label">Notas</span>
          <textarea
            className="input"
            rows={2}
            value={notes}
            onChange={(e) => setNotes(e.target.value)}
          />
        </label>
      </div>
      <div className="form-actions">
        <button type="submit" className="btn btn-primary" disabled={saving}>
          {saving ? "Guardando…" : "Capturar movimiento"}
        </button>
        <button type="button" className="btn btn-outline" onClick={onCancel}>
          Cancelar
        </button>
      </div>
    </form>
  );
}

function Pager({
  page,
  pageCount,
  onChange,
}: {
  page: number;
  pageCount: number;
  onChange: (p: number) => void;
}) {
  return (
    <nav className="pager" aria-label="Paginación">
      <button
        type="button"
        className="btn btn-outline"
        onClick={() => onChange(page - 1)}
        disabled={page <= 1}
      >
        ← Anterior
      </button>
      <span className="pager-info">
        Página <strong>{page}</strong> de {pageCount}
      </span>
      <button
        type="button"
        className="btn btn-outline"
        onClick={() => onChange(page + 1)}
        disabled={page >= pageCount}
      >
        Siguiente →
      </button>
    </nav>
  );
}

function ListSkeleton() {
  return (
    <div className="cust-list" aria-hidden>
      {Array.from({ length: 8 }).map((_, i) => (
        <div className="skeleton skel-row" key={i} />
      ))}
    </div>
  );
}

function EmptyState({ query }: { query: string }) {
  return (
    <div className="state-box">
      <div className="state-glyph" aria-hidden>
        ⌕
      </div>
      <h3>Sin resultados</h3>
      <p>
        {query
          ? `No encontramos movimientos para “${query}”.`
          : "No hay movimientos que coincidan con los filtros."}
      </p>
    </div>
  );
}
