"use client";

import { useCallback, useEffect, useRef, useState } from "react";
import Link from "next/link";
import { AppShell } from "@/components/AppShell";
import { ContextReports } from "@/components/ContextReports";
import { MovementForm } from "@/components/MovementForm";
import {
  getBillingFacets,
  getBillingStats,
  listBalances,
  listMovements,
  resolveOutstanding,
  voidMovement,
} from "@/lib/api";
import { useCan } from "@/lib/abilities";
import {
  balancePhrase,
  balanceTone,
  directionLabel,
  domainLabel,
  formatDate,
  formatMoney,
  formatNumber,
  ledgerSourceLabel,
  SIN_NOMBRE,
  txTypeLabel,
} from "@/lib/labels";
import type {
  BalanceFilter,
  BalanceListItem,
  BalanceListResponse,
  BalanceSort,
  BillingFacets,
  BillingStats,
  LedgerCurrency,
  LedgerDirection,
  MovementListItem,
  MovementListResponse,
  MovementSort,
  TransactionDomain,
} from "@/lib/types";

/**
 * Shared billing / statements browser — plan step 6.
 *
 * Two views over the same ledger, because staff ask two different questions:
 *   - "Saldos": who owes what, one row per customer. The receivables worklist.
 *   - "Movimientos": every individual charge and credit, filterable — the
 *     answer to "what did we bill for water in April".
 *
 * Both are cross-line: a customer's utility charges and insurance movements sit
 * in the same ledger, which is the point of the unified customer record.
 *
 * Balances are always shown *per currency* and never added together — see the
 * currency note in `billing.service.ts`.
 */
type View = "saldos" | "movimientos";

const BALANCE_FILTERS: { key: BalanceFilter; label: string }[] = [
  { key: "owing", label: "Con adeudo" },
  { key: "credit", label: "Con saldo a favor" },
  { key: "settled", label: "En ceros" },
  { key: "all", label: "Todos" },
];

const BALANCE_SORTS: { key: BalanceSort; label: string }[] = [
  { key: "owing_desc", label: "Mayor adeudo primero" },
  { key: "credit_desc", label: "Mayor saldo a favor primero" },
  { key: "recent", label: "Movimiento más reciente" },
  { key: "customer", label: "Cliente (A–Z)" },
];

const MOVEMENT_SORTS: { key: MovementSort; label: string }[] = [
  { key: "date_desc", label: "Fecha (más reciente)" },
  { key: "date_asc", label: "Fecha (más antigua)" },
  { key: "amount_asc", label: "Cargo más grande" },
  { key: "amount_desc", label: "Abono más grande" },
  { key: "customer", label: "Cliente (A–Z)" },
];

const DIRECTIONS: { key: LedgerDirection | ""; label: string }[] = [
  { key: "", label: "Cargos y abonos" },
  { key: "charge", label: "Sólo cargos" },
  { key: "credit", label: "Sólo abonos" },
];

const DOMAINS: { key: TransactionDomain | ""; label: string }[] = [
  { key: "", label: "Ambas líneas" },
  { key: "UTILITY", label: "Servicios" },
  { key: "INSURANCE", label: "Seguros" },
];

export default function EstadoCuentaPage() {
  return (
    <AppShell>
      <BillingBrowser />
    </AppShell>
  );
}

function BillingBrowser() {
  const canCapture = useCan("ledger:create");
  const canVoid = useCan("ledger:void");
  const [stats, setStats] = useState<BillingStats | null>(null);
  const [facets, setFacets] = useState<BillingFacets | null>(null);
  const [view, setView] = useState<View>("saldos");

  // The currency every balance figure is filtered and sorted on. MXN is the
  // default because the charge side of the ledger is MXN-only.
  const [currency, setCurrency] = useState<LedgerCurrency>("MXN");
  const [query, setQuery] = useState("");
  const [domain, setDomain] = useState<TransactionDomain | "">("");

  const [balanceFilter, setBalanceFilter] = useState<BalanceFilter>("owing");
  const [balanceSort, setBalanceSort] = useState<BalanceSort>("owing_desc");

  const [direction, setDirection] = useState<LedgerDirection | "">("");
  const [typeId, setTypeId] = useState("");
  const [source, setSource] = useState("");
  // "" = no filter, "true" = only NOPAGO rows, "false" = only settled ones.
  const [outstanding, setOutstanding] = useState<"" | "true" | "false">("");
  const [from, setFrom] = useState("");
  const [to, setTo] = useState("");
  const [movementSort, setMovementSort] = useState<MovementSort>("date_desc");

  const [balances, setBalances] = useState<BalanceListResponse | null>(null);
  const [movements, setMovements] = useState<MovementListResponse | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);
  const [captureOpen, setCaptureOpen] = useState(false);
  const [resolving, setResolving] = useState<MovementListItem | null>(null);

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

  useEffect(() => {
    getBillingStats().then(setStats).catch(() => setStats(null));
    getBillingFacets().then(setFacets).catch(() => setFacets(null));
  }, []);

  const runSearch = useCallback(
    (p: number) => {
      setLoading(true);
      setError(null);
      const done = (fn: () => void) => {
        fn();
        setLoading(false);
      };
      if (view === "saldos") {
        listBalances({
          query: query || undefined,
          currency,
          balance: balanceFilter,
          domain: domain || undefined,
          sort: balanceSort,
          page: p,
          pageSize: 25,
        })
          .then((res) => done(() => setBalances(res)))
          .catch((e) => {
            setError(e?.message ?? "No se pudieron cargar los saldos.");
            setLoading(false);
          });
      } else {
        listMovements({
          query: query || undefined,
          currency,
          domain: domain || undefined,
          direction: direction || undefined,
          typeId: typeId || undefined,
          source: source || undefined,
          outstanding: outstanding === "" ? undefined : outstanding === "true",
          from: from || undefined,
          to: to || undefined,
          sort: movementSort,
          page: p,
          pageSize: 25,
        })
          .then((res) => done(() => setMovements(res)))
          .catch((e) => {
            setError(e?.message ?? "No se pudieron cargar los movimientos.");
            setLoading(false);
          });
      }
    },
    [
      view,
      query,
      currency,
      domain,
      balanceFilter,
      balanceSort,
      direction,
      typeId,
      source,
      outstanding,
      from,
      to,
      movementSort,
    ],
  );

  useEffect(() => {
    if (debounceRef.current) clearTimeout(debounceRef.current);
    debounceRef.current = setTimeout(() => runSearch(1), 280);
    return () => {
      if (debounceRef.current) clearTimeout(debounceRef.current);
    };
  }, [runSearch]);

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

  /** Jumping in from a headline count should land on the matching worklist. */
  function pickBalance(f: BalanceFilter, cur?: LedgerCurrency) {
    setView("saldos");
    setBalanceFilter(f);
    if (cur) setCurrency(cur);
    setBalanceSort(f === "credit" ? "credit_desc" : "owing_desc");
  }

  const data = view === "saldos" ? balances : movements;
  const filtered =
    query !== "" ||
    domain !== "" ||
    (view === "saldos"
      ? balanceFilter !== "owing" || balanceSort !== "owing_desc"
      : direction !== "" ||
        typeId !== "" ||
        source !== "" ||
        from !== "" ||
        to !== "" ||
        movementSort !== "date_desc");

  function clearFilters() {
    setQuery("");
    setDomain("");
    setCurrency("MXN");
    setBalanceFilter("owing");
    setBalanceSort("owing_desc");
    setDirection("");
    setTypeId("");
    setSource("");
    setFrom("");
    setTo("");
    setMovementSort("date_desc");
  }

  return (
    <>
      <div className="page-head rise">
        <p className="eyebrow">Cobranza y facturación</p>
        <h1 className="page-title">Estado de cuenta</h1>
        <BillingStatStrip
          stats={stats}
          currency={currency}
          balanceFilter={view === "saldos" ? balanceFilter : null}
          onPickBalance={pickBalance}
        />
        <LedgerTotalsStrip stats={stats} />
      </div>

      <div style={{ marginTop: 12 }}>
        <ContextReports
          entries={[
            { slug: "listado-en-rojo", label: "En rojo" },
            { slug: "reporte-de-efectivo", label: "Reporte de efectivo" },
          ]}
        />
      </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={
              view === "saldos"
                ? "Buscar por cliente o ciudad…"
                : "Buscar por cliente, referencia, cheque, concepto…"
            }
            aria-label="Buscar en el estado de cuenta"
          />
        </div>
        <div className="seg" role="tablist" aria-label="Vista">
          {(
            [
              { key: "saldos" as View, label: "Saldos por cliente" },
              { key: "movimientos" as View, label: "Movimientos" },
            ]
          ).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 && (
          <div style={{ display: "flex", gap: 10 }}>
            <Link href="/estado-cuenta/lote" className="btn btn-outline">
              Captura por cheque
            </Link>
            <button
              type="button"
              className="btn btn-primary"
              onClick={() => setCaptureOpen((v) => !v)}
            >
              {captureOpen ? "Cerrar captura" : "Capturar movimiento"}
            </button>
          </div>
        )}
      </div>

      {view === "movimientos" && resolving && (
        <ResolveDialog
          movement={resolving}
          onCancel={() => setResolving(null)}
          onDone={() => {
            setResolving(null);
            runSearch(movements?.page ?? 1);
            getBillingStats()
              .then(setStats)
              .catch(() => setStats(null));
          }}
        />
      )}

      {view === "movimientos" && captureOpen && (
        <section className="section">
          <div className="section-head">
            <span className="section-rule cuenta" aria-hidden />
            <h2 className="section-title">Capturar movimiento</h2>
          </div>
          <MovementForm
            concepts={facets?.types ?? []}
            defaultCurrency={currency}
            onSaved={() => {
              setCaptureOpen(false);
              runSearch(movements?.page ?? 1);
              getBillingStats()
                .then(setStats)
                .catch(() => setStats(null));
            }}
            onCancel={() => setCaptureOpen(false)}
          />
        </section>
      )}

      <div className="filter-row">
        <label className="filter-field">
          <span className="filter-label">Moneda</span>
          <select
            className="input select"
            value={currency}
            onChange={(e) => setCurrency(e.target.value as LedgerCurrency)}
          >
            <option value="MXN">Pesos (MXN)</option>
            <option value="USD">Dólares (USD)</option>
          </select>
        </label>

        <label className="filter-field">
          <span className="filter-label">Línea de negocio</span>
          <select
            className="input select"
            value={domain}
            onChange={(e) => setDomain(e.target.value as TransactionDomain | "")}
          >
            {DOMAINS.map((d) => (
              <option key={d.key} value={d.key}>
                {d.label}
              </option>
            ))}
          </select>
        </label>

        {view === "saldos" ? (
          <>
            <label className="filter-field">
              <span className="filter-label">Saldo</span>
              <select
                className="input select"
                value={balanceFilter}
                onChange={(e) =>
                  setBalanceFilter(e.target.value as BalanceFilter)
                }
              >
                {BALANCE_FILTERS.map((b) => (
                  <option key={b.key} value={b.key}>
                    {b.label}
                  </option>
                ))}
              </select>
            </label>
            <label className="filter-field">
              <span className="filter-label">Ordenar por</span>
              <select
                className="input select"
                value={balanceSort}
                onChange={(e) => setBalanceSort(e.target.value as BalanceSort)}
              >
                {BALANCE_SORTS.map((s) => (
                  <option key={s.key} value={s.key}>
                    {s.label}
                  </option>
                ))}
              </select>
            </label>
          </>
        ) : (
          <>
            <label className="filter-field">
              <span className="filter-label">Movimiento</span>
              <select
                className="input select"
                value={direction}
                onChange={(e) =>
                  setDirection(e.target.value as LedgerDirection | "")
                }
              >
                {DIRECTIONS.map((d) => (
                  <option key={d.key} value={d.key}>
                    {d.label}
                  </option>
                ))}
              </select>
            </label>

            <label className="filter-field">
              <span className="filter-label">Concepto</span>
              <select
                className="input select"
                value={typeId}
                onChange={(e) => setTypeId(e.target.value)}
              >
                <option value="">Todos los conceptos</option>
                {facets?.types.map((t) => (
                  <option key={t.id} value={t.id}>
                    {txTypeLabel({ nameEn: t.name })} ({formatNumber(t.count)})
                  </option>
                ))}
              </select>
            </label>

            <label className="filter-field">
              <span className="filter-label">Origen</span>
              <select
                className="input select"
                value={source}
                onChange={(e) => setSource(e.target.value)}
              >
                <option value="">Todos los orígenes</option>
                {facets?.sources.map((s) => (
                  <option key={s.name} value={s.name}>
                    {ledgerSourceLabel(s.name)} ({formatNumber(s.count)})
                  </option>
                ))}
              </select>
            </label>

            <label className="filter-field">
              <span className="filter-label">Estado de pago</span>
              <select
                className="input select"
                value={outstanding}
                onChange={(e) =>
                  setOutstanding(e.target.value as "" | "true" | "false")
                }
              >
                <option value="">Todos</option>
                <option value="true">Sin fondos (pendientes)</option>
                <option value="false">Pagados</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={movementSort}
                onChange={(e) =>
                  setMovementSort(e.target.value as MovementSort)
                }
              >
                {MOVEMENT_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>

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

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

      {error ? (
        <div className="state-error" role="alert">
          {error}
        </div>
      ) : loading ? (
        <ListSkeleton />
      ) : data && data.total === 0 ? (
        <EmptyState query={query} view={view} />
      ) : view === "saldos" ? (
        <>
          <div className="cust-list">
            {balances?.items.map((b) => (
              <BalanceRow key={b.id} b={b} currency={currency} />
            ))}
          </div>
          {balances && balances.pageCount > 1 && (
            <Pager
              page={balances.page}
              pageCount={balances.pageCount}
              onChange={goToPage}
            />
          )}
        </>
      ) : (
        <>
          <div className="card">
            <div className="tx-scroll">
              <table className="tx-table">
                <thead>
                  <tr>
                    <th>Fecha</th>
                    <th>Cliente</th>
                    <th>Línea</th>
                    <th>Concepto</th>
                    <th>Referencia</th>
                    <th className="num">Monto</th>
                    {(canVoid || canCapture) && (
                      <th style={{ width: 1, whiteSpace: "nowrap" }}>Acciones</th>
                    )}
                  </tr>
                </thead>
                <tbody>
                  {movements?.items.map((m) => (
                    <MovementRow
                      key={m.id}
                      m={m}
                      canVoid={canVoid}
                      canCapture={canCapture}
                      onResolve={setResolving}
                      onVoided={() => {
                        runSearch(movements?.page ?? 1);
                        getBillingStats()
                          .then(setStats)
                          .catch(() => setStats(null));
                      }}
                    />
                  ))}
                </tbody>
              </table>
            </div>
          </div>
          {movements && movements.pageCount > 1 && (
            <Pager
              page={movements.page}
              pageCount={movements.pageCount}
              onChange={goToPage}
            />
          )}
        </>
      )}
    </>
  );
}

/** Headline counts; the owing/credit cells double as worklist shortcuts. */
function BillingStatStrip({
  stats,
  currency,
  balanceFilter,
  onPickBalance,
}: {
  stats: BillingStats | null;
  currency: LedgerCurrency;
  balanceFilter: BalanceFilter | null;
  onPickBalance: (f: BalanceFilter, cur?: LedgerCurrency) => 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>
    );
  }

  const cur = stats.byCurrency.find((c) => c.currency === currency);

  return (
    <div className="stat-strip">
      <button
        type="button"
        className={`stat-cell stat-cell-btn accent${
          balanceFilter === "owing" ? " selected" : ""
        }`}
        onClick={() => onPickBalance("owing")}
        aria-pressed={balanceFilter === "owing"}
      >
        <div className="stat-value">{formatNumber(cur?.owing ?? 0)}</div>
        <div className="stat-label">Clientes con adeudo ({currency})</div>
      </button>
      <button
        type="button"
        className={`stat-cell stat-cell-btn${
          balanceFilter === "credit" ? " selected" : ""
        }`}
        onClick={() => onPickBalance("credit")}
        aria-pressed={balanceFilter === "credit"}
      >
        <div className="stat-value">{formatNumber(cur?.inCredit ?? 0)}</div>
        <div className="stat-label">Con saldo a favor ({currency})</div>
      </button>
      <button
        type="button"
        className={`stat-cell stat-cell-btn${
          balanceFilter === "all" ? " selected" : ""
        }`}
        onClick={() => onPickBalance("all")}
        aria-pressed={balanceFilter === "all"}
      >
        <div className="stat-value">{formatNumber(stats.ledgerCustomers)}</div>
        <div className="stat-label">Clientes con movimientos</div>
      </button>
      <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>
      <div className="stat-cell">
        <div className="stat-value">
          {formatNumber(stats.crossLineCustomers)}
        </div>
        <div className="stat-label">Con movimientos en ambas líneas</div>
      </div>
    </div>
  );
}

/**
 * Charges vs credits for the whole ledger, per currency. Kept as two separate
 * chips rather than one figure: the two currencies are never added together.
 */
function LedgerTotalsStrip({ stats }: { stats: BillingStats | null }) {
  if (!stats || stats.byCurrency.length === 0) return null;
  return (
    <div className="mix-strip">
      <span className="premium-caption">Movimiento histórico</span>
      {stats.byCurrency.map((c) => (
        <div className="ledger-chip" key={c.currency}>
          <span className="ledger-chip-cur">{c.currency}</span>
          <span className="ledger-chip-figs">
            <span className="tx-amount neg">
              {formatMoney(c.charges, c.currency)}
            </span>
            <span className="ledger-chip-label">
              en cargos · {formatNumber(c.chargeCount)}
            </span>
          </span>
          <span className="ledger-chip-figs">
            <span className="tx-amount pos">
              {formatMoney(c.credits, c.currency)}
            </span>
            <span className="ledger-chip-label">
              en abonos · {formatNumber(c.creditCount)}
            </span>
          </span>
        </div>
      ))}
    </div>
  );
}

/** Totals for everything the current movement filter matched, not just the page. */
function FilteredTotals({
  totals,
}: {
  totals: MovementListResponse["totals"];
}) {
  if (totals.length === 0) return null;
  return (
    <div className="filtered-totals">
      {totals.map((t) => (
        <div className="filtered-total" key={t.currency}>
          <span className="filtered-total-cur">{t.currency}</span>
          <span>
            <strong className="tx-amount neg">
              {formatMoney(t.charges, t.currency)}
            </strong>{" "}
            cargos
          </span>
          <span>
            <strong className="tx-amount pos">
              {formatMoney(t.credits, t.currency)}
            </strong>{" "}
            abonos
          </span>
          <span className="filtered-total-net">
            Neto <strong>{formatMoney(t.net, t.currency)}</strong>
          </span>
        </div>
      ))}
    </div>
  );
}

function BalanceRow({
  b,
  currency,
}: {
  b: BalanceListItem;
  currency: LedgerCurrency;
}) {
  const selected =
    b.balances.find((x) => x.currency === currency) ?? b.balances[0];
  const other = b.balances.find((x) => x.currency !== currency);
  const tone = balanceTone(selected.balance);
  const location = [b.city?.replace(/,\s*$/, ""), b.state]
    .filter(Boolean)
    .join(", ");

  return (
    <Link href={`/estado-cuenta/${b.id}`} className="cust-row bal-row">
      <div className="cust-main">
        <div className="cust-name">
          <span className={b.name === SIN_NOMBRE ? "cust-name-missing" : undefined}>
            {b.name}
          </span>
          {b.utilityMovements > 0 && (
            <span className="badge badge-servicios">
              <span className="dot" /> Servicios
            </span>
          )}
          {b.insuranceMovements > 0 && (
            <span className="badge badge-seguros">
              <span className="dot" /> Seguros
            </span>
          )}
        </div>
        <div className="cust-sub">
          {location && <span>{location}</span>}
          {location && <span className="sep">·</span>}
          <span>
            {formatNumber(b.movements)}{" "}
            {b.movements === 1 ? "movimiento" : "movimientos"}
          </span>
          <span className="sep">·</span>
          <span>último {formatDate(b.lastMovement)}</span>
        </div>
      </div>
      <div className="bal-side">
        <div className={`bal-amount ${tone}`}>
          {formatMoney(selected.balance, selected.currency)}
        </div>
        <div className={`bal-phrase ${tone}`}>
          {balancePhrase(selected.balance)} · {selected.currency}
        </div>
        {other && Math.abs(Number(other.balance)) >= 0.005 && (
          <div className="bal-other">
            {formatMoney(other.balance, other.currency)} en {other.currency}
          </div>
        )}
      </div>
    </Link>
  );
}

function MovementRow({
  m,
  canVoid,
  canCapture,
  onVoided,
  onResolve,
}: {
  m: MovementListItem;
  canVoid: boolean;
  canCapture: boolean;
  onVoided: () => void;
  onResolve: (m: MovementListItem) => 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 voidMovement(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>
        <Link href={`/estado-cuenta/${m.customerId}`} className="inline-link">
          <span
            className={
              m.customerName === SIN_NOMBRE ? "cust-name-missing" : undefined
            }
          >
            {m.customerName}
          </span>
        </Link>
      </td>
      <td className="tx-domain-cell">
        <span className={`tx-dot ${m.domain}`} />
        {domainLabel(m.domain)}
      </td>
      <td>
        {txTypeLabel(m.type)}
        {m.message && <div className="tx-concept">{m.message}</div>}
      </td>
      <td className="tx-ref">
        {m.reference || m.checkNumber || "—"}
        <div className="tx-concept">{ledgerSourceLabel(m.source)}</div>
      </td>
      <td className="num">
        <span className={`tx-amount ${m.direction === "charge" ? "neg" : "pos"}`}>
          {formatMoney(m.amount, m.currency)}
        </span>
        <div className="tx-cur">
          {m.currency} · {directionLabel(m.direction)}
          {m.outstanding && !m.voided && (
            <>
              {" · "}
              <span className="tx-outstanding">sin fondos</span>
            </>
          )}
        </div>
      </td>
      {(canVoid || canCapture) && (
        <td style={{ whiteSpace: "nowrap" }}>
          {/* Resolver only makes sense on a live outstanding row, and it's a
              capture action (completing one), not a void. */}
          {!m.voided && m.outstanding && canCapture && (
            <button
              type="button"
              className="btn btn-ghost"
              style={{ padding: "4px 10px", fontSize: 12 }}
              onClick={() => onResolve(m)}
            >
              Resolver
            </button>
          )}
          {!m.voided && canVoid && (
            <button
              type="button"
              className="btn btn-ghost"
              style={{ padding: "4px 10px", fontSize: 12 }}
              onClick={doVoid}
              disabled={busy}
            >
              {busy ? "Anulando…" : "Anular"}
            </button>
          )}
        </td>
      )}
    </tr>
  );
}

/**
 * Resolve an outstanding row: the check finally got cut. Takes the check number
 * and the date it was paid, which also becomes the movement's date — the legacy
 * behavior, since the ledger date is when money actually moved.
 */
function ResolveDialog({
  movement,
  onDone,
  onCancel,
}: {
  movement: MovementListItem;
  onDone: () => void;
  onCancel: () => void;
}) {
  const [checkNumber, setCheckNumber] = useState("");
  const [resolvedDate, setResolvedDate] = useState(
    new Date().toISOString().slice(0, 10),
  );
  const [busy, setBusy] = useState(false);
  const [error, setError] = useState<string | null>(null);

  async function submit(e: React.FormEvent) {
    e.preventDefault();
    if (!checkNumber.trim()) {
      setError("Indica el número de cheque.");
      return;
    }
    setBusy(true);
    setError(null);
    try {
      await resolveOutstanding(movement.id, {
        checkNumber: checkNumber.trim(),
        resolvedDate,
      });
      onDone();
    } catch (e2) {
      setError((e2 as Error)?.message ?? "No se pudo resolver el movimiento.");
      setBusy(false);
    }
  }

  return (
    <div className="card" style={{ padding: 20, marginBottom: 16 }}>
      <h2 className="section-title" style={{ marginBottom: 6 }}>
        Resolver movimiento sin fondos
      </h2>
      <p className="muted" style={{ marginBottom: 14 }}>
        {movement.customerName} · {formatMoney(movement.amount, movement.currency)}{" "}
        {movement.currency}
        {movement.reference ? ` · ${movement.reference}` : ""}
      </p>
      {error && <div className="state-box state-error">{error}</div>}
      <form onSubmit={submit}>
        <div className="form-grid">
          <label className="field">
            <span className="field-label">Número de cheque *</span>
            <input
              className="input"
              value={checkNumber}
              onChange={(e) => setCheckNumber(e.target.value)}
              autoFocus
            />
          </label>
          <label className="field">
            <span className="field-label">Fecha de pago *</span>
            <input
              className="input"
              type="date"
              required
              value={resolvedDate}
              onChange={(e) => setResolvedDate(e.target.value)}
            />
          </label>
        </div>
        <p className="muted" style={{ fontSize: 13, marginTop: 10 }}>
          El movimiento tomará esta fecha y empezará a contar en el saldo del
          cliente.
        </p>
        <div className="form-actions">
          <button type="submit" className="btn btn-primary" disabled={busy}>
            {busy ? "Resolviendo…" : "Resolver"}
          </button>
          <button type="button" className="btn btn-outline" onClick={onCancel}>
            Cancelar
          </button>
        </div>
      </form>
    </div>
  );
}

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, view }: { query: string; view: View }) {
  return (
    <div className="state-box">
      <div className="state-glyph" aria-hidden>
        ⌕
      </div>
      <h3>Sin resultados</h3>
      <p>
        {query
          ? `No encontramos ${
              view === "saldos" ? "clientes" : "movimientos"
            } para “${query}”.`
          : `No hay ${
              view === "saldos" ? "saldos" : "movimientos"
            } que coincidan con los filtros.`}
      </p>
    </div>
  );
}
