"use client";

/**
 * General ledger (PRD 6.6 → 6.5, 6.8), on the Medora design system and the Accounts API Part 1 (DECISIONS.md,
 * 26 Oct 2026). The design's chart of accounts, account statement, journal editor, trial balance, profit and loss,
 * balance sheet (three periods), opening balances and transactions list (`components/medora/ledger.tsx`,
 * `journal.tsx`, `transactions.tsx`, `ledger-entries.tsx`).
 *
 * The ledger starts when an Admin posts the opening balances (Q1), pre-filled from Medora's own stock, receivable and
 * payable figures; documents from then on post automatically. Documents that couldn't post are listed under
 * "Waiting to post" (Q3). Not built yet (later Accounts plans): closing periods, supplier bills, reconciliations,
 * claims, quotations, petty cash, assets, the executive report.
 */

import * as React from "react";
import { PlusIcon, RefreshCwIcon } from "lucide-react";

import { PanelError } from "@/components/medora/errors";
import { MedoraPageHeader } from "@/components/medora/page-header";
import { ConfirmDialog } from "@/components/medora/confirm-dialog";
import { RecordDrawer } from "@/components/medora/drawer";
import { AccountStatement, BalanceSheet, ChartOfAccounts, OpeningBalances, ProfitAndLoss, TrialBalance, accountProblems, type AccountType, type LedgerAccount, type OpeningLine } from "@/components/medora/ledger";
import { JournalEditor, journalTotals, lineProblems, newLine, type JournalLine } from "@/components/medora/journal";
import { TransactionsList, type MoneyTxn } from "@/components/medora/transactions";
import { LedgerEntries } from "@/components/medora/ledger-entries";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
import { Spinner } from "@/components/ui/spinner";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { toast } from "@/components/ui/toast";
import { BranchesApi, LedgerApi, type LedgerAccountRow, type LedgerTxn } from "@/lib/api";
import { useTabParam, validTab } from "@/lib/use-tab-param";
import { addDays, formatDate, formatKES, localToday } from "@/lib/format";
import { useMenuPermissions } from "@/lib/permissions";
import { Surface } from "@/components/medora/surface";
import { Loading } from "@/components/medora/loading";

const PAGE = "Accounts/General ledger";
const apiError = (e: unknown, fallback: string): string => {
  const m = (e as { response?: { data?: { message?: string | string[] } } })?.response?.data?.message;
  return Array.isArray(m) ? m.join(" ") : m || fallback;
};
const usePerm = () => {
  const { can, loaded } = useMenuPermissions();
  return (action: string) => loaded && can(PAGE, action);
};
const toDesign = (a: LedgerAccountRow): LedgerAccount => ({ code: a.code, name: a.name, type: a.type, parent: a.parent, branch: a.branch, description: a.description, system: a.system, active: a.active, balance: a.balance, hasPostings: a.hasPostings });
const monthStart = (iso: string) => `${iso.slice(0, 7)}-01`;
const monthEnd = (iso: string) => addDays(`${addDays(`${iso.slice(0, 7)}-28`, 4).slice(0, 7)}-01`, -1);
const lastYear = (iso: string) => `${Number(iso.slice(0, 4)) - 1}${iso.slice(4)}`;

export default function LedgerPage() {
  const may = usePerm();
  const { loaded } = useMenuPermissions();
  const canRead = may("Read");
  const [started, setStarted] = React.useState<string | null | undefined>(undefined);
  const [accounts, setAccounts] = React.useState<LedgerAccountRow[] | null>(null);
  const [accountsFailed, setAccountsFailed] = React.useState(false); // R12 (card 037): a failed load says so, with Try again
  const [branches, setBranches] = React.useState<{ id: number; name: string }[]>([]);
  const [branchId, setBranchId] = React.useState<number | null>(null);
  const [asAt, setAsAt] = React.useState(localToday());
  const [tabParam, setTab] = useTabParam(); // page tabs change the URL (tabs spec 7.1)
  const tab = validTab(tabParam, ["openings", "accounts", "transactions", "statements", "pending", "journal"], "accounts");
  // R4 (card 037): the three statements share one tab, switched inside it
  const [statement, setStatement] = React.useState<"trial" | "pl" | "bs">("trial");
  const addAccount = React.useRef<(() => void) | null>(null);
  const [version, setVersion] = React.useState(0);
  const [pendingCount, setPendingCount] = React.useState(0);
  const refresh = () => setVersion((v) => v + 1);

  React.useEffect(() => {
    if (!canRead) return;
    LedgerApi.openings()
      .then((o) => {
        setStarted(o.started);
        if (!o.started) setTab("openings");
      })
      .catch((e) => toast.add({ type: "error", title: "Couldn't load the ledger", description: apiError(e, "Try again.") }));
    LedgerApi.pending().then((p) => setPendingCount(p.length)).catch(() => undefined);
    if (may("All branches")) BranchesApi.getAll().then((b: { id: number; name: string }[]) => setBranches(b)).catch(() => undefined);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [canRead, version]);
  React.useEffect(() => {
    if (!canRead) return;
    setAccounts(null);
    LedgerApi.accounts({ asAt, branchId })
      .then((a) => (setAccounts(a), setAccountsFailed(false)))
      .catch((e) => {
        setAccounts([]);
        setAccountsFailed(true);
        toast.add({ type: "error", title: "Couldn't load the accounts", description: apiError(e, "Try again.") });
      });
  }, [canRead, asAt, branchId, version]);

  if (loaded && !canRead)
    return (
      <div className="medora mx-auto w-full max-w-6xl py-10 text-center text-sm text-muted-foreground">Your role can&apos;t see the general ledger.</div>
    );

  return (
    <div className="medora mx-auto w-full max-w-6xl space-y-4">
      <MedoraPageHeader
        title="General ledger"
        description={started === undefined ? "Loading…" : started ? `Books started ${formatDate(started)}; documents post to them automatically.` : "Not started: post the opening balances to start the books."}
        primary={started && may("Create") ? { label: "New journal", icon: PlusIcon, onSelect: () => setTab("journal") } : undefined}
        secondary={tab === "accounts" && may("Update") ? [{ label: "Add account", icon: PlusIcon, onSelect: () => addAccount.current?.() }] : []}
      />
      <Surface className="flex flex-wrap items-end gap-3">
        <Field className="w-44">
          <FieldLabel htmlFor="gl-asat">Balances as at</FieldLabel>
          <Input id="gl-asat" type="date" value={asAt} max={localToday()} onChange={(e) => e.target.value && setAsAt(e.target.value)} />
        </Field>
        {branches.length ? (
          <Field className="w-56">
            <FieldLabel htmlFor="gl-branch">Branch</FieldLabel>
            <NativeSelect id="gl-branch" value={branchId ?? ""} onChange={(e) => setBranchId(e.target.value ? Number(e.target.value) : null)}>
              <NativeSelectOption value="">All branches</NativeSelectOption>
              {branches.map((b) => (
                <NativeSelectOption key={b.id} value={b.id}>
                  {b.name}
                </NativeSelectOption>
              ))}
            </NativeSelect>
          </Field>
        ) : null}
        {branchId != null ? <p className="pb-2 text-xs text-muted-foreground">A branch shows only that branch&apos;s postings; opening balances and goods received belong to all branches.</p> : null}
      </Surface>
      <Tabs value={tab} onValueChange={(v) => setTab(String(v))}>
        <TabsList variant="line" className="w-full justify-start border-b border-border">
          {!started ? <TabsTrigger value="openings">Opening balances</TabsTrigger> : null}
          <TabsTrigger value="accounts">Chart of accounts</TabsTrigger>
          <TabsTrigger value="transactions">Transactions</TabsTrigger>
          <TabsTrigger value="statements">Statements</TabsTrigger>
          <TabsTrigger value="pending">Waiting to post{pendingCount ? ` (${pendingCount})` : ""}</TabsTrigger>
        </TabsList>
        <div className="mt-4">
          {!started ? (
            <TabsContent value="openings">
              <Openings accounts={accounts} canPost={may("Approve")} onPosted={() => { refresh(); setTab("accounts"); }} />
            </TabsContent>
          ) : null}
          <TabsContent value="accounts">{accountsFailed ? <PanelError framed what="the chart of accounts" onRetry={refresh} /> : accounts ? <Chart accounts={accounts} asAt={asAt} branchId={branchId} canEdit={may("Update")} onChanged={refresh} addRef={addAccount} /> : <Loading what="the chart of accounts" columns={4} rows={10} numeric={[3]} />}</TabsContent>
          <TabsContent value="transactions">
            <Transactions branchId={branchId} canReverse={may("Approve")} version={version} onChanged={refresh} />
          </TabsContent>
          {started && may("Create") ? (
            <TabsContent value="journal">
              {accounts ? <NewJournal accounts={accounts} onPosted={() => { refresh(); setTab("transactions"); }} /> : <Loading what="the journal" shape="form" rows={5} />}
            </TabsContent>
          ) : null}
          <TabsContent value="statements" className="space-y-4">
            <Tabs value={statement} onValueChange={(v) => setStatement(v as typeof statement)}>
              <TabsList>
                <TabsTrigger value="trial">Trial balance</TabsTrigger>
                <TabsTrigger value="pl">Profit and loss</TabsTrigger>
                <TabsTrigger value="bs">Balance sheet</TabsTrigger>
              </TabsList>
            </Tabs>
            {statement === "trial" ? accounts ? <TrialTab accounts={accounts} asAt={asAt} branchId={branchId} /> : <Loading what="the trial balance" columns={4} rows={10} numeric={[2, 3]} footer /> : null}
            {statement === "pl" ? <ProfitTab asAt={asAt} branchId={branchId} version={version} /> : null}
            {statement === "bs" ? <BalanceTab asAt={asAt} branchId={branchId} version={version} /> : null}
          </TabsContent>
          <TabsContent value="pending">
            <Pending canRetry={may("Create")} version={version} onChanged={refresh} />
          </TabsContent>
        </div>
      </Tabs>
    </div>
  );
}

/* ------------------------------------ Opening balances (AC-06, Q1) ------------------------------------ */

function Openings({ accounts, canPost, onPosted }: { accounts: LedgerAccountRow[] | null; canPost: boolean; onPosted: () => void }) {
  const [lines, setLines] = React.useState<Record<string, OpeningLine>>({});
  const [notes, setNotes] = React.useState<Record<string, string>>({});
  const [date, setDate] = React.useState(localToday());
  const [confirm, setConfirm] = React.useState(false);
  React.useEffect(() => {
    if (!accounts) return;
    LedgerApi.openings()
      .then((o) => {
        const byId = new Map(accounts.map((a) => [a.id, a.code]));
        const init: Record<string, OpeningLine> = {};
        const n: Record<string, string> = {};
        for (const l of o.lines) {
          const code = byId.get(l.accountId);
          if (!code) continue;
          init[code] = { code, debit: l.debit ? String(l.debit) : "", credit: l.credit ? String(l.credit) : "" };
          n[code] = l.from;
        }
        setLines(init);
        setNotes(n);
      })
      .catch(() => undefined);
  }, [accounts]);
  if (!accounts) return <Loading what="the journal" shape="form" rows={5} />;
  const num = (s: string) => Number(String(s).replace(/,/g, "")) || 0;
  const dr = Object.values(lines).reduce((t, l) => t + num(l.debit), 0);
  const cr = Object.values(lines).reduce((t, l) => t + num(l.credit), 0);
  const diff = Math.round((dr - cr) * 100) / 100;
  const opening = accounts.find((a) => a.code === "3900");
  const both = Object.values(lines).some((l) => num(l.debit) && num(l.credit));
  const balanceTo3900 = () => {
    if (!opening) return;
    const cur = lines[opening.code] ?? { code: opening.code, debit: "", credit: "" };
    const net = diff - num(cur.debit) + num(cur.credit); // what's left without 3900
    setLines({ ...lines, [opening.code]: { code: opening.code, debit: net < 0 ? (-net).toFixed(2) : "", credit: net > 0 ? net.toFixed(2) : "" } });
  };
  return (
    <div className="space-y-4">
      <div className="rounded-lg border border-[var(--color-border-panel)] bg-card p-4 text-sm">
        <p className="font-medium">The books start from these balances.</p>
        <p className="mt-1 text-muted-foreground">
          Stock, what customers owe and what we owe suppliers are filled in from Medora. Type in the cash in each drawer, the M-Pesa till balances and the bank, and anything else from your old books. The difference goes to
          3900 Opening balances. Once posted, every sale, return, payment and expense from that moment posts by itself. Opening balances are posted once; later corrections are journals.
        </p>
        {Object.keys(notes).length ? (
          <ul className="mt-2 list-disc pl-5 text-xs text-muted-foreground">
            {Object.entries(notes).map(([code, from]) => (
              <li key={code}>
                {code}: {from}
              </li>
            ))}
          </ul>
        ) : null}
      </div>
      <OpeningBalances accounts={accounts.map(toDesign)} lines={lines} onChange={(code, l) => setLines({ ...lines, [code]: l })} />
      <div className="flex flex-wrap items-end gap-3">
        <Field className="w-44">
          <FieldLabel htmlFor="ob-date">Dated</FieldLabel>
          <Input id="ob-date" type="date" value={date} max={localToday()} onChange={(e) => setDate(e.target.value)} />
        </Field>
        <Button variant="outline" disabled={!opening} onClick={balanceTo3900}>
          Put the difference on 3900 Opening balances
        </Button>
        {canPost ? (
          <Button disabled={diff !== 0 || both || dr === 0} onClick={() => setConfirm(true)}>
            Post opening balances
          </Button>
        ) : (
          <p className="text-xs text-muted-foreground">Posting them needs the Approve permission on General ledger.</p>
        )}
      </div>
      <ConfirmDialog
        open={confirm}
        onOpenChange={setConfirm}
        level="B"
        tone="primary"
        title="Post the opening balances and start the books?"
        consequences={[`${formatKES(dr)} on each side, dated ${formatDate(date)}.`, "From now on every sale, return, payment, goods received, stock adjustment, drop and expense posts automatically.", "Opening balances can be posted only once; corrections are journals."]}
        actionLabel="Post and start"
        busyLabel="Posting…"
        onConfirm={async () => {
          const byCode = new Map(accounts.map((a) => [a.code, a.id]));
          try {
            await LedgerApi.postOpenings({ date, lines: Object.values(lines).filter((l) => num(l.debit) || num(l.credit)).map((l) => ({ accountId: byCode.get(l.code)!, debit: num(l.debit), credit: num(l.credit) })) });
          } catch (e) {
            throw new Error(apiError(e, "Not posted. Try again."));
          }
          toast.add({ title: "The books have started" });
          onPosted();
        }}
      />
    </div>
  );
}

/* ------------------------------------ Chart of accounts (AC-01) and statements (AC-05) ------------------------------------ */

type AccountForm = { id?: number; code: string; name: string; type: AccountType; parent: string; description: string; system?: boolean };

function Chart({ accounts, asAt, branchId, canEdit, onChanged, addRef }: { accounts: LedgerAccountRow[]; asAt: string; branchId: number | null; canEdit: boolean; onChanged: () => void; addRef: React.RefObject<(() => void) | null> }) {
  const [form, setForm] = React.useState<AccountForm | null>(null);
  // R2 (card 037): "Add account" is in the page header and opens the form through this
  React.useEffect(() => {
    addRef.current = () => setForm({ code: "", name: "", type: "Expense", parent: "", description: "" });
    return () => {
      addRef.current = null;
    };
  });
  const [statementOf, setStatementOf] = React.useState<LedgerAccountRow | null>(null);
  const [deleting, setDeleting] = React.useState<LedgerAccountRow | null>(null);
  const byCode = new Map(accounts.map((a) => [a.code, a]));
  const setActive = async (a: LedgerAccountRow, active: boolean) => {
    try {
      await LedgerApi.updateAccount(a.id, { active });
      toast.add({ title: `${a.code} ${a.name} ${active ? "reactivated" : "deactivated"}` });
      onChanged();
    } catch (e) {
      toast.add({ type: "error", title: "Not changed", description: apiError(e, "Try again.") });
    }
  };
  return (
    <div className="space-y-3">
      <ChartOfAccounts
        accounts={accounts.map(toDesign)}
        asAt={formatDate(asAt)}
        actionsFor={(d) => {
          const a = byCode.get(d.code)!;
          return [
            { label: "Statement", onSelect: () => setStatementOf(a) },
            ...(canEdit ? [{ label: "Edit", onSelect: () => setForm({ id: a.id, code: a.code, name: a.name, type: a.type, parent: a.parent ?? "", description: a.description ?? "", system: a.system }) }] : []),
            ...(canEdit && !a.system ? [a.active ? { label: "Deactivate", onSelect: () => void setActive(a, false) } : { label: "Reactivate", onSelect: () => void setActive(a, true) }] : []),
            ...(canEdit && !a.system && !a.hasPostings ? [{ label: "Delete…", destructive: true, onSelect: () => setDeleting(a) }] : []),
          ];
        }}
      />
      {form ? <AccountDialog form={form} accounts={accounts} onClose={() => setForm(null)} onSaved={() => { setForm(null); onChanged(); }} /> : null}
      {statementOf ? <StatementDrawer account={statementOf} branchId={branchId} asAt={asAt} onClose={() => setStatementOf(null)} /> : null}
      <ConfirmDialog
        open={!!deleting}
        onOpenChange={(o) => !o && setDeleting(null)}
        level="B"
        title={`Delete ${deleting?.code} ${deleting?.name}?`}
        consequences={["It has never been posted to. This can't be undone."]}
        actionLabel="Delete account"
        busyLabel="Deleting…"
        onConfirm={async () => {
          if (!deleting) return;
          try {
            await LedgerApi.deleteAccount(deleting.id);
          } catch (e) {
            throw new Error(apiError(e, "Not deleted."));
          }
          setDeleting(null);
          onChanged();
        }}
      />
    </div>
  );
}

function AccountDialog({ form, accounts, onClose, onSaved }: { form: AccountForm; accounts: LedgerAccountRow[]; onClose: () => void; onSaved: () => void }) {
  const [d, setD] = React.useState(form);
  const [tried, setTried] = React.useState(false);
  const [saving, setSaving] = React.useState(false);
  const p = accountProblems({ code: d.code, name: d.name, type: d.type, parent: d.parent || undefined, branch: accounts.find((a) => a.id === form.id)?.branch }, accounts.map(toDesign), form.id ? form.code : undefined);
  const parents = accounts.filter((a) => a.type === d.type && a.code !== d.code && !a.parent);
  const locked = !!form.system || !!accounts.find((a) => a.id === form.id)?.hasPostings;
  const save = async () => {
    setTried(true);
    if (Object.keys(p).length) return;
    setSaving(true);
    try {
      const body = { code: d.code, name: d.name.trim(), type: d.type, parentCode: d.parent || null, description: d.description.trim() || null };
      if (form.id) await LedgerApi.updateAccount(form.id, body);
      else await LedgerApi.addAccount(body);
      toast.add({ title: form.id ? "Account saved" : "Account added" });
      onSaved();
    } catch (e) {
      toast.add({ type: "error", title: "Not saved", description: apiError(e, "Try again.") });
    } finally {
      setSaving(false);
    }
  };
  const err = (k: "code" | "name" | "parent") => (tried && p[k] ? <FieldError>{p[k]}</FieldError> : null);
  return (
    <Dialog open onOpenChange={(o) => !o && !saving && onClose()}>
      <DialogContent>
        <DialogHeader>
          <DialogTitle>{form.id ? `Edit ${form.code} ${form.name}` : "New account"}</DialogTitle>
          <DialogDescription>{locked ? "This account is used by automatic postings or already has postings: it can be renamed and described, not renumbered." : "Assets 1000–1999, liabilities 2000–2999, equity 3000–3999, income 4000–4999, expenses 5000–6999."}</DialogDescription>
        </DialogHeader>
        <div className="grid gap-4">
          <Field>
            <FieldLabel htmlFor="ac-type">Type</FieldLabel>
            <NativeSelect id="ac-type" value={d.type} disabled={locked} onChange={(e) => setD({ ...d, type: e.target.value as AccountType, parent: "" })}>
              {(["Asset", "Liability", "Equity", "Income", "Expense"] as const).map((t) => (
                <NativeSelectOption key={t} value={t}>
                  {t}
                </NativeSelectOption>
              ))}
            </NativeSelect>
          </Field>
          <Field data-invalid={(tried && !!p.code) || undefined}>
            <FieldLabel htmlFor="ac-code">Account number</FieldLabel>
            <Input id="ac-code" inputMode="numeric" className="font-mono" disabled={locked} value={d.code} onChange={(e) => setD({ ...d, code: e.target.value.replace(/\D/g, "").slice(0, 4) })} />
            {err("code")}
          </Field>
          <Field data-invalid={(tried && !!p.name) || undefined} className="sm:col-span-2">
            <FieldLabel htmlFor="ac-name">Name</FieldLabel>
            <Input id="ac-name" value={d.name} onChange={(e) => setD({ ...d, name: e.target.value })} />
            {err("name")}
          </Field>
          <Field data-invalid={(tried && !!p.parent) || undefined} className="sm:col-span-2">
            <FieldLabel htmlFor="ac-parent">Sub-account of</FieldLabel>
            <NativeSelect id="ac-parent" value={d.parent} disabled={locked} onChange={(e) => setD({ ...d, parent: e.target.value })}>
              <NativeSelectOption value="">None (a main account)</NativeSelectOption>
              {parents.map((a) => (
                <NativeSelectOption key={a.code} value={a.code}>
                  {a.code} {a.name}
                </NativeSelectOption>
              ))}
            </NativeSelect>
            {err("parent") ?? <FieldDescription>A sub-account shares its parent&apos;s first two digits.</FieldDescription>}
          </Field>
          <Field className="sm:col-span-2">
            <FieldLabel htmlFor="ac-desc">Description</FieldLabel>
            <Input id="ac-desc" value={d.description} onChange={(e) => setD({ ...d, description: e.target.value })} />
          </Field>
        </div>
        <DialogFooter>
          <Button variant="outline" disabled={saving} onClick={onClose}>
            Cancel
          </Button>
          <Button disabled={saving} onClick={() => void save()}>
            {saving ? <Spinner /> : null}
            Save
          </Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

function StatementDrawer({ account, branchId, asAt, onClose }: { account: LedgerAccountRow; branchId: number | null; asAt: string; onClose: () => void }) {
  const [from, setFrom] = React.useState(monthStart(asAt));
  const [to, setTo] = React.useState(asAt);
  const [s, setS] = React.useState<Awaited<ReturnType<typeof LedgerApi.statement>> | null>(null);
  const [failed, setFailed] = React.useState(false); // R12 (card 037): a failed load says so, with Try again
  const [attempt, setAttempt] = React.useState(0);
  React.useEffect(() => {
    setS(null);
    LedgerApi.statement(account.id, { from, to, branchId })
      .then((r) => (setS(r), setFailed(false)))
      .catch((e) => (setFailed(true), toast.add({ type: "error", title: "Couldn't load the statement", description: apiError(e, "Try again.") })));
  }, [account.id, from, to, branchId, attempt]);
  return (
    <RecordDrawer open onOpenChange={(o) => !o && onClose()} variant="detail" width="lg" title={`${account.code} ${account.name}`}>
      <div className="space-y-4">
        <div className="flex flex-wrap gap-3">
          <Field className="w-44">
            <FieldLabel htmlFor="st-from">From</FieldLabel>
            <Input id="st-from" type="date" value={from} onChange={(e) => e.target.value && setFrom(e.target.value)} />
          </Field>
          <Field className="w-44">
            <FieldLabel htmlFor="st-to">To</FieldLabel>
            <Input id="st-to" type="date" value={to} onChange={(e) => e.target.value && setTo(e.target.value)} />
          </Field>
        </div>
        {failed ? <PanelError what="the statement" onRetry={() => setAttempt((n) => n + 1)} /> : s ? <AccountStatement account={toDesign(account)} opening={s.opening} postings={s.postings} formatDate={(iso) => formatDate(iso)} from={formatDate(from)} to={formatDate(to)} /> : <Loading what="the statement" columns={6} rows={8} numeric={[3, 4, 5]} />}
      </div>
    </RecordDrawer>
  );
}

/* ------------------------------------ Transactions (AC-14) and reversal (AC-04) ------------------------------------ */

function Transactions({ branchId, canReverse, version, onChanged }: { branchId: number | null; canReverse: boolean; version: number; onChanged: () => void }) {
  const today = localToday();
  const [from, setFrom] = React.useState(monthStart(today));
  const [to, setTo] = React.useState(today);
  const [data, setData] = React.useState<{ total: number; rows: LedgerTxn[] } | null>(null);
  const [open, setOpen] = React.useState<LedgerTxn | null>(null);
  const [reversing, setReversing] = React.useState(false);
  const [failed, setFailed] = React.useState(false); // R12 (card 037): a failed load says so, with Try again
  const [attempt, setAttempt] = React.useState(0);
  React.useEffect(() => {
    setData(null);
    LedgerApi.transactions({ from, to, branchId, pageSize: 200 })
      .then((d) => (setData(d), setFailed(false)))
      .catch((e) => {
        setData({ total: 0, rows: [] });
        setFailed(true);
        toast.add({ type: "error", title: "Couldn't load transactions", description: apiError(e, "Try again.") });
      });
  }, [from, to, branchId, version, attempt]);
  const rows: MoneyTxn[] = (data?.rows ?? []).map((r) => ({ ref: r.number, date: r.date, kind: r.status === "Reversed" ? `${r.source} (reversed)` : r.source, details: r.reference ? `${r.details} · ${r.reference}` : r.details, debit: r.debit, credit: r.credit, amount: r.amount, branch: r.branch }));
  return (
    <div className="space-y-3">
      {/* One container (data table spec §1): the dates sit first in the transactions toolbar */}
      <TransactionsList
        rows={data ? rows : []}
        pageSize={15}
        onOpen={(t) => setOpen(data?.rows.find((r) => r.number === t.ref) ?? null)}
        toolbarStart={
          <>
            <Input aria-label="From" type="date" className="w-40" value={from} onChange={(e) => e.target.value && setFrom(e.target.value)} />
            <Input aria-label="To" type="date" className="w-40" value={to} onChange={(e) => e.target.value && setTo(e.target.value)} />
          </>
        }
        body={failed ? <PanelError what="transactions" onRetry={() => setAttempt((n) => n + 1)} /> : !data ? <Loading what="transactions" columns={6} rows={8} numeric={[5]} bare /> : undefined}
      />
      {data && data.total > data.rows.length ? <p className="text-xs text-muted-foreground">Showing the latest {data.rows.length} of {data.total}. Narrow the dates to see the rest.</p> : null}
      <Dialog open={!!open} onOpenChange={(o) => !o && setOpen(null)}>
        <DialogContent className="sm:max-w-2xl">
          <DialogHeader>
            <DialogTitle>
              {open?.number}: {open?.details}
            </DialogTitle>
            <DialogDescription>
              {open?.source}
              {open?.by ? `, by ${open.by}` : ""}
              {open?.reference ? `, ${open.reference}` : ""}. Entries are never edited: a mistake is reversed by an equal and opposite entry.
            </DialogDescription>
          </DialogHeader>
          {open ? <LedgerEntries document={open.number} date={formatDate(open.date)} status={open.status} lines={open.lines.map((l) => ({ account: l.account, debit: l.debit || undefined, credit: l.credit || undefined, memo: l.memo ?? undefined }))} /> : null}
          <DialogFooter>
            {canReverse && open && open.status === "Posted" && !open.reversalOf ? (
              <Button variant="outline" onClick={() => setReversing(true)}>
                Reverse…
              </Button>
            ) : null}
            <Button onClick={() => setOpen(null)}>Close</Button>
          </DialogFooter>
        </DialogContent>
      </Dialog>
      <ConfirmDialog
        open={reversing}
        onOpenChange={setReversing}
        level="B"
        title={`Reverse ${open?.number}?`}
        consequences={["An equal and opposite entry is posted today. The original stays, marked reversed.", "Reversing a sale's entry doesn't void the sale: void it at the till instead."]}
        actionLabel="Reverse"
        busyLabel="Reversing…"
        reasonHelp="Shown on the reversing entry."
        onConfirm={async ({ reason }) => {
          if (!open) return;
          try {
            await LedgerApi.reverse(open.id, reason ?? "");
          } catch (e) {
            throw new Error(apiError(e, "Not reversed."));
          }
          toast.add({ title: `${open.number} reversed` });
          setOpen(null);
          onChanged();
        }}
      />
    </div>
  );
}

/* ------------------------------------ Manual journal (AC-04) ------------------------------------ */

function NewJournal({ accounts, onPosted }: { accounts: LedgerAccountRow[]; onPosted: () => void }) {
  const [lines, setLines] = React.useState<JournalLine[]>([newLine(), newLine()]);
  const [date, setDate] = React.useState(localToday());
  const [details, setDetails] = React.useState("");
  const [tried, setTried] = React.useState(false);
  const [saving, setSaving] = React.useState(false);
  const usable = accounts.filter((a) => a.active && !accounts.some((k) => k.parent === a.code));
  const t = journalTotals(lines);
  const filled = lines.filter((l) => l.account || l.debit || l.credit);
  const bad = filled.some((l) => lineProblems(l).length) || t.difference !== 0 || t.debit <= 0 || !details.trim() || !date;
  const post = async () => {
    setTried(true);
    if (bad) return;
    setSaving(true);
    try {
      const byCode = new Map(accounts.map((a) => [a.code, a.id]));
      const n = (s: string) => Number(s.replace(/,/g, "")) || 0;
      await LedgerApi.postJournal({ date, details: details.trim(), lines: filled.map((l) => ({ accountId: byCode.get(l.account)!, debit: n(l.debit), credit: n(l.credit), memo: l.memo || undefined })) });
      toast.add({ title: "Journal posted" });
      onPosted();
    } catch (e) {
      toast.add({ type: "error", title: "Not posted", description: apiError(e, "Try again.") });
    } finally {
      setSaving(false);
    }
  };
  return (
    <div className="space-y-4">
      <div className="grid gap-4">
        <Field>
          <FieldLabel htmlFor="jn-date">Date</FieldLabel>
          <Input id="jn-date" type="date" value={date} onChange={(e) => setDate(e.target.value)} />
          {date && date < localToday() ? <FieldDescription>Dated in the past: it is logged as such.</FieldDescription> : null}
        </Field>
        <Field className="sm:col-span-2" data-invalid={(tried && !details.trim()) || undefined}>
          <FieldLabel htmlFor="jn-details">Details</FieldLabel>
          <Input id="jn-details" value={details} onChange={(e) => setDetails(e.target.value)} placeholder="For example: owner put money into the bank" />
          {tried && !details.trim() ? <FieldError>Say what the journal is for.</FieldError> : null}
        </Field>
      </div>
      <JournalEditor accounts={usable.map((a) => ({ code: a.code, name: a.branch ? `${a.name}, ${a.branch}` : a.name, type: a.type }))} lines={lines} onChange={setLines} showProblems={tried} disabled={saving} />
      <div className="flex justify-end">
        <Button disabled={saving} onClick={() => void post()}>
          {saving ? <Spinner /> : null}
          Post journal
        </Button>
      </div>
    </div>
  );
}

/* ------------------------------------ Statements (AC-05) ------------------------------------ */

function TrialTab({ accounts, asAt, branchId }: { accounts: LedgerAccountRow[]; asAt: string; branchId: number | null }) {
  const [tb, setTb] = React.useState<Awaited<ReturnType<typeof LedgerApi.trialBalance>> | null>(null);
  React.useEffect(() => {
    LedgerApi.trialBalance({ asAt, branchId }).then(setTb).catch(() => setTb(null));
  }, [asAt, branchId, accounts]);
  return (
    <div className="space-y-2">
      {tb && !tb.balanced ? <p className="text-sm font-medium text-status-error-text">The ledger&apos;s own totals differ ({formatKES(tb.debit)} against {formatKES(tb.credit)}). This should never happen: tell your administrator.</p> : null}
      {/* Each account's own balance from the API, parents included: a parent such as 4000 Sales carries postings of its
          own, which the chart's rolled-up balances (and the design's leaf-only rule) would leave out */}
      {tb ? (
        <TrialBalance accounts={tb.lines.map((l) => ({ code: l.code, name: l.name, type: l.type as AccountType, active: true, balance: l.balance }))} asAt={formatDate(asAt)} />
      ) : (
        <Loading what="the trial balance" columns={4} rows={10} numeric={[2, 3]} footer />
      )}
    </div>
  );
}

function ProfitTab({ asAt, branchId, version }: { asAt: string; branchId: number | null; version: number }) {
  const [compare, setCompare] = React.useState<"month" | "year">("month");
  const [data, setData] = React.useState<Awaited<ReturnType<typeof LedgerApi.profitAndLoss>> | null>(null);
  const [failed, setFailed] = React.useState(false); // R12 (card 037): a failed load says so, with Try again
  const [attempt, setAttempt] = React.useState(0);
  const periods = React.useMemo(() => {
    if (compare === "year") {
      const y = Number(asAt.slice(0, 4));
      return [0, 1, 2].map((k) => ({ from: `${y - k}-01-01`, to: k === 0 ? asAt : `${y - k}-12-31`, label: k === 0 ? `${y} to ${formatDate(asAt, { short: true })}` : String(y - k) }));
    }
    const prev = addDays(monthStart(asAt), -1);
    return [
      { from: monthStart(asAt), to: asAt, label: `${formatDate(monthStart(asAt), { short: true })} to ${formatDate(asAt, { short: true })}` },
      { from: monthStart(prev), to: prev, label: formatDate(prev).replace(/^\d+\s/, "") },
      { from: monthStart(lastYear(asAt)), to: monthEnd(lastYear(asAt)), label: formatDate(lastYear(asAt)).replace(/^\d+\s/, "") },
    ];
  }, [asAt, compare]);
  React.useEffect(() => {
    setData(null);
    LedgerApi.profitAndLoss(periods, branchId)
      .then((r) => (setData(r), setFailed(false)))
      .catch((e) => (setFailed(true), toast.add({ type: "error", title: "Couldn't load profit and loss", description: apiError(e, "Try again.") })));
  }, [periods, branchId, version, attempt]);
  return (
    <div className="space-y-3">
      <Tabs value={compare} onValueChange={(v) => setCompare(v as "month" | "year")}>
        <TabsList>
          <TabsTrigger value="month">This month, last month, a year ago</TabsTrigger>
          <TabsTrigger value="year">Three years</TabsTrigger>
        </TabsList>
      </Tabs>
      {failed ? <PanelError framed what="profit and loss" onRetry={() => setAttempt((n) => n + 1)} /> : data ? <ProfitAndLoss periods={periods.map((p) => p.label)} income={data.income} costOfSales={data.costOfSales} expenses={data.expenses} /> : <Loading what="profit and loss" columns={4} rows={9} numeric={[1, 2, 3]} footer />}
      <p className="text-xs text-muted-foreground">From the ledger, so it starts when the books started. Reports keeps its own profit and loss until it reads from here.</p>
    </div>
  );
}

function BalanceTab({ asAt, branchId, version }: { asAt: string; branchId: number | null; version: number }) {
  const dates = React.useMemo(() => [asAt, `${Number(asAt.slice(0, 4)) - 1}-12-31`, `${Number(asAt.slice(0, 4)) - 2}-12-31`], [asAt]);
  const [data, setData] = React.useState<Awaited<ReturnType<typeof LedgerApi.balanceSheet>> | null>(null);
  const [failed, setFailed] = React.useState(false); // R12 (card 037): a failed load says so, with Try again
  const [attempt, setAttempt] = React.useState(0);
  React.useEffect(() => {
    setData(null);
    LedgerApi.balanceSheet(dates, branchId)
      .then((r) => (setData(r), setFailed(false)))
      .catch((e) => (setFailed(true), toast.add({ type: "error", title: "Couldn't load the balance sheet", description: apiError(e, "Try again.") })));
  }, [dates, branchId, version, attempt]);
  if (failed) return <PanelError framed what="the balance sheet" onRetry={() => setAttempt((n) => n + 1)} />;
  if (!data) return <Loading what="the balance sheet" columns={4} rows={9} numeric={[1, 2, 3]} footer />;
  return (
    <div className="space-y-2">
      <BalanceSheet periods={dates.map((d) => formatDate(d))} assets={data.assets} liabilities={data.liabilities} equity={data.equity} />
      <p className="text-xs text-muted-foreground">Equity includes the profit to date, worked out from the ledger&apos;s own income and expenses. There is no balancing figure.</p>
    </div>
  );
}

/* ------------------------------------ Waiting to post (Q3) ------------------------------------ */

function Pending({ canRetry, version, onChanged }: { canRetry: boolean; version: number; onChanged: () => void }) {
  const [rows, setRows] = React.useState<Awaited<ReturnType<typeof LedgerApi.pending>> | null>(null);
  const [busy, setBusy] = React.useState(false);
  React.useEffect(() => {
    LedgerApi.pending().then(setRows).catch(() => setRows([]));
  }, [version]);
  if (!rows) return <Loading what="postings waiting" columns={5} rows={4} />;
  return (
    <div className="space-y-3">
      <div className="flex flex-wrap items-center justify-between gap-2">
        <p className="text-sm text-muted-foreground">Documents that are saved but couldn&apos;t post, with why. They are tried again each time the server starts.</p>
        {canRetry && rows.length ? (
          <Button
            size="sm"
            variant="outline"
            disabled={busy}
            onClick={async () => {
              setBusy(true);
              try {
                const r = await LedgerApi.retryPending();
                toast.add({ title: `${r.posted} of ${r.tried} posted` });
                onChanged();
              } finally {
                setBusy(false);
              }
            }}
          >
            <RefreshCwIcon data-icon="inline-start" />
            Try again
          </Button>
        ) : null}
      </div>
      {rows.length ? (
        <ul className="divide-y divide-[var(--color-border-subtle)] rounded-lg border border-[var(--color-border-panel)] bg-card text-sm">
          {rows.map((r) => (
            <li key={r.id} className="p-3">
              <span className="font-medium">
                {r.source} #{r.sourceId}
              </span>
              <span className="block text-muted-foreground">{r.error}</span>
              <span className="text-xs text-muted-foreground">
                Tried {r.attempts} time{r.attempts === 1 ? "" : "s"}, last {formatDate(r.updatedAt)}
              </span>
            </li>
          ))}
        </ul>
      ) : (
        <p className="text-sm">Nothing is waiting: every document has posted.</p>
      )}
    </div>
  );
}
