"use client";

import * as React from "react";
import { ChevronDownIcon, ChevronRightIcon, CircleCheckIcon, ClockIcon, CircleMinusIcon, CircleXIcon, SearchIcon, TriangleAlertIcon } from "lucide-react";

import { DiffView, type FieldChange } from "@/components/medora/diff-view";
import { CsvImportReview, downloadText, FileUpload, reviewCsv, type CsvReviewResult } from "@/components/medora/file-upload";
import { kes } from "@/components/medora/payroll";
import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { Input } from "@/components/ui/input";
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
import { addDays, count, formatDate } from "@/lib/format";
import { cn } from "@/lib/utils";

/*
 * Record lists and their reports: the "list what happened" half of features whose forms already exist.
 * IN-05 shelf allocation by CSV, IN-07 merges, IN-08 product history, IN-20 waste, IN-21 conversions,
 * AC-09 invoices and customer payments, AC-11 supplier payments and credit notes, AC-12 reconciliations,
 * AC-15 quotations, RP-02 stock as at a date, RP-10 supplier purchases. All share one list table.
 */

/** `total` is computed from the rows on screen, so a filtered list never shows an unfiltered total. */
export type Col<T> = { key: string; header: string; numeric?: boolean; cell: (r: T) => React.ReactNode; total?: (shown: T[]) => React.ReactNode };

/**
 * List table: search, select filters, optional expandable rows and a totals footer. One container (data table spec,
 * 02-data-display §1): the toolbar sits inside it, above the table, sharing its border; never inside another card.
 * `exports` is the toolbar's Export menu (§1.8): each format gets the rows the search and filters show.
 * `toolbarStart` puts the page's own filters (dates, a branch) first in the same toolbar; `body` replaces the rows
 * while they load or when they fail, so the toolbar stays where it is (§1, initial loading: real header and toolbar).
 */
export function ListTable<T>({
  rows,
  cols,
  rowKey,
  search,
  filters = [],
  expand,
  totalsLabel,
  empty = "Nothing matches.",
  minWidth = "min-w-[640px]",
  exports = [],
  toolbarStart,
  body,
}: {
  rows: T[];
  cols: Col<T>[];
  rowKey: (r: T) => string;
  search?: { placeholder: string; text: (r: T) => string };
  filters?: { label: string; all: string; value: (r: T) => string }[];
  expand?: (r: T) => React.ReactNode;
  totalsLabel?: (shown: T[]) => React.ReactNode;
  empty?: string;
  minWidth?: string;
  exports?: { label: string; onSelect: (shown: T[]) => void }[];
  toolbarStart?: React.ReactNode;
  body?: React.ReactNode;
}) {
  const [q, setQ] = React.useState("");
  const [f, setF] = React.useState<Record<string, string>>({});
  const [open, setOpen] = React.useState<string | null>(null);
  const term = q.trim().toLowerCase();
  const shown = rows.filter((r) => (!term || !search || search.text(r).toLowerCase().includes(term)) && filters.every((x) => !f[x.label] || x.value(r) === f[x.label]));
  const hasTotals = cols.some((c) => c.total !== undefined) || totalsLabel;
  return (
    <div className="overflow-hidden rounded-md border border-[var(--color-border-panel)] bg-card">
      {search || filters.length || exports.length || toolbarStart ? (
        <div className="flex flex-wrap items-center gap-2 border-b border-[var(--color-border-subtle)] p-3">
          {toolbarStart}
          {search ? (
            <div className="relative min-w-52 flex-1">
              <SearchIcon className="pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground" aria-hidden />
              <Input aria-label="Search" placeholder={search.placeholder} className="pl-8" value={q} onChange={(e) => setQ(e.target.value)} />
            </div>
          ) : null}
          {filters.map((x) => (
            <NativeSelect key={x.label} aria-label={x.label} value={f[x.label] ?? ""} onChange={(e) => setF({ ...f, [x.label]: e.target.value })}>
              <NativeSelectOption value="">{x.all}</NativeSelectOption>
              {[...new Set(rows.map(x.value))].sort().map((o) => (
                <NativeSelectOption key={o} value={o}>
                  {o}
                </NativeSelectOption>
              ))}
            </NativeSelect>
          ))}
          {exports.length ? (
            <DropdownMenu>
              <DropdownMenuTrigger render={<Button size="sm" variant="outline" className="ml-auto" disabled={!shown.length} />}>
                Export
                <ChevronDownIcon data-icon="inline-end" />
              </DropdownMenuTrigger>
              <DropdownMenuContent align="end">
                {exports.map((x) => (
                  <DropdownMenuItem key={x.label} onClick={() => x.onSelect(shown)}>
                    {x.label}
                  </DropdownMenuItem>
                ))}
              </DropdownMenuContent>
            </DropdownMenu>
          ) : null}
        </div>
      ) : null}
      {body ?? (
      <div className="overflow-x-auto">
        <table className={cn("w-full text-sm", minWidth)}>
          <thead className="bg-[var(--color-bg-surface-subtle)] text-left text-xs text-muted-foreground">
            <tr>
              {expand ? <th className="w-8" /> : null}
              {cols.map((c) => (
                <th key={c.key} className={cn("px-2 py-2 font-medium", c.numeric && "text-right")}>
                  {c.header}
                </th>
              ))}
            </tr>
          </thead>
          {shown.map((r) => {
            const k = rowKey(r);
            const isOpen = open === k;
            return (
              <tbody key={k} className="border-t border-[var(--color-border-subtle)]">
                <tr className={cn("align-top", isOpen && "bg-muted/30")}>
                  {expand ? (
                    <td className="px-1 py-1.5">
                      <Button size="icon-sm" variant="ghost" aria-expanded={isOpen} aria-label={`${isOpen ? "Hide" : "Show"} details for ${k}`} onClick={() => setOpen(isOpen ? null : k)}>
                        <ChevronRightIcon className={cn("transition-transform", isOpen && "rotate-90")} />
                      </Button>
                    </td>
                  ) : null}
                  {cols.map((c) => (
                    <td key={c.key} className={cn("px-2 py-2", c.numeric && "text-right tabular-nums")}>
                      {c.cell(r)}
                    </td>
                  ))}
                </tr>
                {expand && isOpen ? (
                  <tr>
                    <td />
                    <td colSpan={cols.length} className="px-2 pb-3">
                      {expand(r)}
                    </td>
                  </tr>
                ) : null}
              </tbody>
            );
          })}
          {!shown.length ? (
            <tbody>
              <tr>
                <td colSpan={cols.length + (expand ? 1 : 0)} className="px-3 py-8 text-center text-muted-foreground">
                  {empty}
                </td>
              </tr>
            </tbody>
          ) : null}
          {hasTotals && shown.length ? (
            <tfoot className="border-t-2 border-[var(--color-border-panel)] bg-muted/30 font-semibold">
              <tr>
                {expand ? <td /> : null}
                {cols.map((c, i) => (
                  <td key={c.key} className={cn("px-2 py-2", c.numeric && "text-right tabular-nums")}>
                    {i === 0 && totalsLabel ? totalsLabel(shown) : c.total?.(shown)}
                  </td>
                ))}
              </tr>
            </tfoot>
          ) : null}
        </table>
      </div>
      )}
    </div>
  );
}

const sum = <T,>(rows: T[], f: (r: T) => number) => rows.reduce((s, r) => s + f(r), 0);

/** Status pill (badge spec, 02-data-display §2): icon and word in a rounded pill; each variant has its own icon shape. */
const TAG = {
  ok: { cls: "border-status-success-border bg-status-success-bg text-status-success-text", Icon: CircleCheckIcon },
  warn: { cls: "border-status-warning-border bg-status-warning-bg text-status-warning-text", Icon: TriangleAlertIcon },
  bad: { cls: "border-status-error-border bg-status-error-bg text-status-error-text", Icon: CircleXIcon },
  info: { cls: "border-status-info-border bg-status-info-bg text-status-info-text", Icon: ClockIcon },
  neutral: { cls: "border-[var(--color-border-panel)] bg-muted text-muted-foreground", Icon: CircleMinusIcon },
} as const;

export function Tag({ tone, children }: { tone: keyof typeof TAG; children: React.ReactNode }) {
  const { cls, Icon } = TAG[tone];
  return (
    <span className={cn("inline-flex items-center gap-1 rounded-full border px-1.5 py-0.5 text-[11px] font-medium whitespace-nowrap", cls)}>
      <Icon className="size-3" aria-hidden />
      {children}
    </span>
  );
}

/**
 * The badge spec's fixed status-to-variant map (02-data-display §2). A word not in it is shown as plain text: new
 * statuses are added to the spec's table first, never chosen per screen.
 */
const STATUS_TONE: Record<string, keyof typeof TAG> = {
  ...Object.fromEntries(["Completed", "Paid", "Approved", "Received", "Sent to KRA", "Balanced", "Connected"].map((w) => [w, "ok"])),
  ...Object.fromEntries(["Partly returned", "Part paid", "Unpaid", "Awaiting approval", "Unmatched", "Shortfall", "Low stock", "Expiring", "Overstocked"].map((w) => [w, "warn"])),
  ...Object.fromEntries(["Overdue", "Over limit", "Failed", "Rejected", "Lost", "Mismatch", "Out of stock", "Expired", "Negative stock", "Error"].map((w) => [w, "bad"])),
  ...Object.fromEntries(["Waiting for customer", "Checking payment", "Queued", "Sent", "Partly received", "Partially Received", "In transit", "Open", "Submitted"].map((w) => [w, "info"])),
  ...Object.fromEntries(["Held", "Returned", "Reversed", "Archived", "Draft", "Cancelled", "Closed", "Not eTIMS", "Preview", "Not connected"].map((w) => [w, "neutral"])),
};

/** A status word as its badge, from the spec's map; plain text when the word isn't in the map. */
export function StatusTag({ status }: { status: string }) {
  const tone = STATUS_TONE[status];
  return tone ? <Tag tone={tone}>{status}</Tag> : <span>{status}</span>;
}

/* ------------------------------------ IN-05 Shelf allocation by CSV ------------------------------------ */

/** `onImport` (Stage 7) saves the checked rows and returns how many products moved; without it the demo only counts them. */
export function ShelfAllocationImport({ products, shelves, sample, onImport }: { products: { code: string; name: string }[]; shelves: string[]; sample?: string; onImport?: (rows: { code: string; shelf: string }[]) => Promise<number> }) {
  const [result, setResult] = React.useState<CsvReviewResult | null>(null);
  const [done, setDone] = React.useState<number | null>(null);
  const check = (text: string) =>
    setResult(
      reviewCsv(
        text,
        [
          { key: "code", header: "Item code", required: true, validate: (v) => (products.some((p) => p.code === v.trim().toUpperCase()) ? undefined : "No product has this code.") },
          { key: "shelf", header: "Shelf", required: true, validate: (v) => (shelves.includes(v.trim().toUpperCase()) ? undefined : `Not a shelf in Settings. Add it there first, or use one of ${shelves.slice(0, 3).join(", ")}…`) },
        ],
        "code",
      ),
    );
  if (done != null)
    return (
      <p className="rounded-md bg-status-success-bg px-3 py-2 text-sm text-status-success-text">
        {count(done, "product")} moved to their shelves. Each change is in the product&apos;s history.{" "}
        <button type="button" className="cursor-pointer underline" onClick={() => (setDone(null), setResult(null))}>
          Import another file
        </button>
      </p>
    );
  return result ? (
    <CsvImportReview result={result} noun="shelf assignments" onImport={() => void (async () => { try { setDone(onImport ? await onImport(result.valid.map((r) => ({ code: r.code, shelf: r.shelf }))) : result.valid.length); } catch { /* the caller says what went wrong; the checked file stays on screen */ } })()} onChooseAnother={() => setResult(null)} />
  ) : (
    <div className="space-y-2">
      <FileUpload
        label="Shelf assignments (CSV)"
        hint="Two columns: Item code, Shelf. One row per product; a product already on a shelf is moved."
        kind="csv"
        accept={[".csv"]}
        maxBytes={2 * 1024 * 1024}
        onTemplate={() => downloadText("shelf-assignments.csv", "Item code,Shelf\nANT-0042,B2\n")}
        upload={async (file, prog) => {
          prog(100);
          check(await file.text());
        }}
      />
      {sample ? (
        <Button size="sm" variant="ghost" onClick={() => check(sample)}>
          Try a sample file
        </Button>
      ) : null}
    </div>
  );
}

/* ------------------------------------ IN-07 Merge report ------------------------------------ */

export type MergeRecord = { ref: string; date: string; kept: string; merged: string; stockMoved: number; salesMoved: number; batchesMoved: number; by: string };

export function MergeReport({ rows }: { rows: MergeRecord[] }) {
  return (
    <ListTable
      rows={rows}
      rowKey={(r) => r.ref}
      search={{ placeholder: "Product or reference", text: (r) => `${r.ref} ${r.kept} ${r.merged}` }}
      cols={[
        { key: "ref", header: "Merge", cell: (r) => (<><span className="font-mono text-xs">{r.ref}</span><span className="block text-xs text-muted-foreground">{formatDate(r.date)} by {r.by}</span></>) },
        { key: "what", header: "Merged into", cell: (r) => (<><span className="text-muted-foreground line-through decoration-1">{r.merged}</span><span className="block font-medium">→ {r.kept}</span></>) },
        { key: "stock", header: "Stock moved", numeric: true, cell: (r) => r.stockMoved.toLocaleString("en-US") },
        { key: "batches", header: "Batches", numeric: true, cell: (r) => r.batchesMoved },
        { key: "sales", header: "Sales history moved", numeric: true, cell: (r) => r.salesMoved.toLocaleString("en-US") },
      ]}
    />
  );
}

/* ------------------------------------ IN-08 Product change history ------------------------------------ */

export type ProductChange = { at: string; who: string; where: string; action: string; changes: FieldChange[] };

export function ProductHistory({ product, entries }: { product: string; entries: ProductChange[] }) {
  return (
    <ol className="space-y-3">
      {entries.map((e, i) => (
        <li key={i}>
          <DiffView action={e.action} record={product} who={e.who} where={e.where} module="Inventory" at={e.at} kind={e.action === "Created" ? "created" : "updated"} changes={e.changes} />
        </li>
      ))}
    </ol>
  );
}

/* ------------------------------------ IN-20 Waste report ------------------------------------ */

export type WasteRecord = { no: string; date: string; branch: string; staff: string; reason: string; items: { name: string; qty: number; cost: number }[]; status: "Awaiting approval" | "Approved" | "Rejected"; approver?: string };
const wasteValue = (w: WasteRecord) => sum(w.items, (i) => i.qty * i.cost);

export function WasteReport({ rows, toolbar, body, empty }: { rows: WasteRecord[]; toolbar?: React.ReactNode; body?: React.ReactNode; empty?: string }) {
  const approved = rows.filter((r) => r.status === "Approved");
  const byReason = [...new Set(approved.map((r) => r.reason))].map((reason) => ({ reason, value: sum(approved.filter((r) => r.reason === reason), wasteValue) })).sort((a, b) => b.value - a.value);
  return (
    <div className="space-y-3">
      <p className="text-sm">
        Written off (approved): <strong className="tabular-nums">{kes(sum(approved, wasteValue))}</strong>
        {byReason.length ? <span className="text-muted-foreground"> · {byReason.map((b) => `${b.reason} ${kes(b.value)}`).join(" · ")}</span> : null}. Awaiting approval: {kes(sum(rows.filter((r) => r.status === "Awaiting approval"), wasteValue))}, not yet off the books.
      </p>
      <ListTable
        rows={rows}
        rowKey={(r) => r.no}
        toolbarStart={toolbar}
        body={body}
        empty={empty}
        filters={[
          { label: "Reason", all: "All reasons", value: (r) => r.reason },
          { label: "Status", all: "Any status", value: (r) => r.status },
          { label: "Branch", all: "All branches", value: (r) => r.branch },
        ]}
        expand={(r) => (
          <ul className="space-y-0.5 text-xs">
            {r.items.map((i) => (
              <li key={i.name} className="flex justify-between gap-3">
                <span>
                  {i.qty} × {i.name} at {kes(i.cost)}
                </span>
                <span className="tabular-nums">{kes(i.qty * i.cost)}</span>
              </li>
            ))}
          </ul>
        )}
        cols={[
          { key: "no", header: "Waste", cell: (r) => (<><span className="font-mono text-xs">{r.no}</span><span className="block text-xs text-muted-foreground">{formatDate(r.date)} · {r.branch}</span></>) },
          { key: "reason", header: "Reason and staff", cell: (r) => (<>{r.reason}<span className="block text-xs text-muted-foreground">{r.staff}</span></>) },
          { key: "status", header: "Status", cell: (r) => (<><Tag tone={r.status === "Approved" ? "ok" : r.status === "Rejected" ? "bad" : "warn"}>{r.status}</Tag>{r.approver ? <span className="mt-0.5 block text-xs text-muted-foreground">by {r.approver}</span> : null}</>) },
          { key: "value", header: "At cost", numeric: true, cell: (r) => kes(wasteValue(r)) },
        ]}
      />
    </div>
  );
}

/* ------------------------------------ IN-21 Conversion report ------------------------------------ */

export type ConversionRecord = { no: string; date: string; parent: string; child: string; parentQty: number; childQty: number; location: string; staff: string };

/** Grouped by the parent product: how many were broken down, into what, where and by whom. */
export function ConversionReport({ rows }: { rows: ConversionRecord[] }) {
  const groups = [...new Set(rows.map((r) => r.parent))].map((parent) => ({ parent, rows: rows.filter((r) => r.parent === parent) }));
  return (
    <div className="space-y-4">
      {groups.map((g) => (
        <section key={g.parent} aria-label={g.parent} className="space-y-1.5">
          <h4 className="flex flex-wrap items-baseline justify-between gap-2 text-sm font-semibold">
            {g.parent}
            <span className="text-xs font-normal text-muted-foreground tabular-nums">
              {sum(g.rows, (r) => r.parentQty)} converted into {sum(g.rows, (r) => r.childQty).toLocaleString("en-US")} {g.rows[0].child}
            </span>
          </h4>
          <ListTable
            rows={g.rows}
            rowKey={(r) => r.no}
            minWidth="min-w-[520px]"
            cols={[
              { key: "no", header: "Conversion", cell: (r) => (<><span className="font-mono text-xs">{r.no}</span><span className="block text-xs text-muted-foreground">{formatDate(r.date)}</span></>) },
              { key: "loc", header: "Where and who", cell: (r) => (<>{r.location}<span className="block text-xs text-muted-foreground">{r.staff}</span></>) },
              { key: "from", header: "Out", numeric: true, cell: (r) => `−${r.parentQty}` },
              { key: "to", header: "In", numeric: true, cell: (r) => `+${r.childQty}` },
            ]}
          />
        </section>
      ))}
    </div>
  );
}

/* ------------------------------------ AC-09 Invoices and customer payments ------------------------------------ */

export type InvoiceRecord = { no: string; date: string; customer: string; dueDate: string; lines: { item: string; qty: number; price: number; vat: number }[]; paid: number };
const invTotal = (i: InvoiceRecord) => Math.round(sum(i.lines, (l) => l.qty * l.price * (1 + l.vat / 100)) * 100) / 100;

/** Invoice report: summary, grouped by customer, or itemised, with paid and outstanding. */
export function InvoiceReport({ rows, today }: { rows: InvoiceRecord[]; today: string }) {
  const [view, setView] = React.useState<"summary" | "grouped" | "itemised">("summary");
  const state = (i: InvoiceRecord) => {
    const due = invTotal(i) - i.paid;
    if (due <= 0.005) return <Tag tone="ok">Paid</Tag>;
    if (i.dueDate < today) return <Tag tone="bad">Overdue</Tag>;
    return <Tag tone={i.paid ? "warn" : "neutral"}>{i.paid ? "Part paid" : "Unpaid"}</Tag>;
  };
  const customers = [...new Set(rows.map((r) => r.customer))].sort();
  return (
    <div className="space-y-3">
      <div role="radiogroup" aria-label="View" className="inline-flex overflow-hidden rounded-md border border-[var(--color-border-panel)] text-sm bg-card">
        {(["summary", "grouped", "itemised"] as const).map((k) => (
          <button key={k} type="button" role="radio" aria-checked={view === k} onClick={() => setView(k)} className={cn("cursor-pointer px-3 py-1.5 capitalize", view === k ? "bg-[var(--color-bg-selected)] font-medium" : "text-muted-foreground hover:bg-muted")}>
            {k === "grouped" ? "By customer" : k}
          </button>
        ))}
      </div>
      {view === "summary" ? (
        <ListTable
          rows={rows}
          rowKey={(r) => r.no}
          filters={[{ label: "Customer", all: "All customers", value: (r) => r.customer }]}
          cols={[
            { key: "no", header: "Invoice", cell: (r) => (<><span className="font-mono text-xs">{r.no}</span><span className="block text-xs text-muted-foreground">{formatDate(r.date)} · due {formatDate(r.dueDate)}</span></>) },
            { key: "c", header: "Customer", cell: (r) => r.customer },
            { key: "s", header: "Status", cell: state },
            { key: "t", header: "Total", numeric: true, cell: (r) => kes(invTotal(r)), total: (s) => kes(sum(s, invTotal)) },
            { key: "p", header: "Paid", numeric: true, cell: (r) => kes(r.paid), total: (s) => kes(sum(s, (r) => r.paid)) },
            { key: "o", header: "Outstanding", numeric: true, cell: (r) => kes(invTotal(r) - r.paid), total: (s) => kes(sum(s, (r) => invTotal(r) - r.paid)) },
          ]}
          totalsLabel={(s) => `${s.length} invoices`}
        />
      ) : view === "grouped" ? (
        <ListTable
          rows={customers.map((c) => ({ c, inv: rows.filter((r) => r.customer === c) }))}
          rowKey={(g) => g.c}
          expand={(g) => (
            <ul className="space-y-0.5 text-xs">
              {g.inv.map((i) => (
                <li key={i.no} className="flex justify-between gap-3">
                  <span>
                    <span className="font-mono">{i.no}</span> · {formatDate(i.date)}
                  </span>
                  <span className="tabular-nums">
                    {kes(invTotal(i))} · owes {kes(invTotal(i) - i.paid)}
                  </span>
                </li>
              ))}
            </ul>
          )}
          cols={[
            { key: "c", header: "Customer", cell: (g) => g.c },
            { key: "n", header: "Invoices", numeric: true, cell: (g) => g.inv.length },
            { key: "t", header: "Invoiced", numeric: true, cell: (g) => kes(sum(g.inv, invTotal)) },
            { key: "o", header: "Outstanding", numeric: true, cell: (g) => kes(sum(g.inv, (r) => invTotal(r) - r.paid)) },
          ]}
        />
      ) : (
        <ListTable
          rows={rows.flatMap((r) => r.lines.map((l, i) => ({ inv: r, l, k: `${r.no}-${i}` })))}
          rowKey={(x) => x.k}
          search={{ placeholder: "Item or invoice", text: (x) => `${x.inv.no} ${x.l.item}` }}
          cols={[
            { key: "no", header: "Invoice", cell: (x) => <span className="font-mono text-xs">{x.inv.no}</span> },
            { key: "item", header: "Item", cell: (x) => x.l.item },
            { key: "q", header: "Qty", numeric: true, cell: (x) => x.l.qty },
            { key: "p", header: "Price", numeric: true, cell: (x) => kes(x.l.price) },
            { key: "v", header: "VAT", numeric: true, cell: (x) => (x.l.vat ? `${x.l.vat}%` : "Exempt") },
            { key: "t", header: "Total", numeric: true, cell: (x) => kes(x.l.qty * x.l.price * (1 + x.l.vat / 100)) },
          ]}
        />
      )}
    </div>
  );
}

export type PaymentRecord = { ref: string; date: string; party: string; method: string; reference?: string; amount: number; appliedTo: string };

/** Customer payment records (AC-09) or supplier payments with a running total per supplier (AC-11). */
export function PaymentRecords({ rows, partyLabel, cumulative }: { rows: PaymentRecord[]; partyLabel: string; cumulative?: boolean }) {
  const sorted = [...rows].sort((a, b) => a.date.localeCompare(b.date) || a.ref.localeCompare(b.ref));
  const running = new Map<string, number>();
  const withCum = sorted.map((r) => {
    const c = (running.get(r.party) ?? 0) + r.amount;
    running.set(r.party, c);
    return { ...r, cum: c };
  });
  return (
    <ListTable
      rows={withCum}
      rowKey={(r) => r.ref}
      filters={[
        { label: partyLabel, all: `All ${partyLabel.toLowerCase()}s`, value: (r) => r.party },
        { label: "Method", all: "Any method", value: (r) => r.method },
      ]}
      cols={[
        { key: "ref", header: "Payment", cell: (r) => (<><span className="font-mono text-xs">{r.ref}</span><span className="block text-xs text-muted-foreground">{formatDate(r.date)}</span></>) },
        { key: "party", header: partyLabel, cell: (r) => r.party },
        { key: "m", header: "Method", cell: (r) => (<>{r.method}{r.reference ? <span className="block font-mono text-xs text-muted-foreground">{r.reference}</span> : null}</>) },
        { key: "a", header: "Applied to", cell: (r) => <span className="text-xs">{r.appliedTo}</span> },
        { key: "amt", header: "Amount", numeric: true, cell: (r) => kes(r.amount), total: (s) => kes(sum(s, (r) => r.amount)) },
        ...(cumulative ? [{ key: "cum", header: "Paid to date", numeric: true, cell: (r: (typeof withCum)[number]) => kes(r.cum) }] : []),
      ]}
      totalsLabel={(s) => count(s.length, "payment")}
    />
  );
}

export type SupplierCreditRecord = { no: string; supplierCn: string; date: string; supplier: string; bill: string; returnRef?: string; amount: number; reason: string };

/** AC-11: supplier credit notes received, each against the bill it reduced. */
export function SupplierCreditNotes({ rows }: { rows: SupplierCreditRecord[] }) {
  return (
    <ListTable
      rows={rows}
      rowKey={(r) => r.no}
      filters={[{ label: "Supplier", all: "All suppliers", value: (r) => r.supplier }]}
      cols={[
        { key: "no", header: "Credit note", cell: (r) => (<><span className="font-mono text-xs">{r.no}</span><span className="block text-xs text-muted-foreground">{formatDate(r.date)} · theirs {r.supplierCn}</span></>) },
        { key: "s", header: "Supplier", cell: (r) => r.supplier },
        { key: "b", header: "Reduced", cell: (r) => (<><span className="font-mono text-xs">{r.bill}</span>{r.returnRef ? <span className="block text-xs text-muted-foreground">for return {r.returnRef}</span> : null}</>) },
        { key: "why", header: "For", cell: (r) => <span className="text-xs">{r.reason}</span> },
        { key: "a", header: "Amount", numeric: true, cell: (r) => kes(r.amount), total: (s) => kes(sum(s, (r) => r.amount)) },
      ]}
      totalsLabel={(s) => count(s.length, "credit note")}
    />
  );
}

/* ------------------------------------ AC-12 Reconciliation report ------------------------------------ */

export type ReconRecord = { ref: string; account: string; endDate: string; statement: number; book: number; matched: number; unmatched: number; by: string; closedOn?: string };

export function ReconciliationReport({ rows }: { rows: ReconRecord[] }) {
  return (
    <ListTable
      rows={rows}
      rowKey={(r) => r.ref}
      filters={[{ label: "Account", all: "All accounts", value: (r) => r.account }]}
      cols={[
        { key: "ref", header: "Reconciliation", cell: (r) => (<><span className="font-mono text-xs">{r.ref}</span><span className="block text-xs text-muted-foreground">to {formatDate(r.endDate)} · {r.by}</span></>) },
        { key: "acc", header: "Account", cell: (r) => r.account },
        { key: "st", header: "Statement", numeric: true, cell: (r) => kes(r.statement) },
        { key: "bk", header: "Books", numeric: true, cell: (r) => kes(r.book) },
        { key: "m", header: "Matched", numeric: true, cell: (r) => `${r.matched} of ${r.matched + r.unmatched}` },
        {
          key: "s",
          header: "Result",
          cell: (r) => {
            const d = Math.round((r.statement - r.book) * 100) / 100;
            return r.closedOn && d === 0 ? <Tag tone="ok">Balanced, closed {formatDate(r.closedOn, { short: true })}</Tag> : d === 0 ? <Tag tone="warn">Balanced, not closed</Tag> : <Tag tone="bad">Off by {kes(Math.abs(d))}</Tag>;
          },
        },
      ]}
    />
  );
}

/* ------------------------------------ AC-15 Quotations ------------------------------------ */

export type Quote = { no: string; date: string; customer: string; total: number; validDays: number; status: "Open" | "Converted" | "Cancelled"; convertedTo?: string; cancelReason?: string };

/** Quotations list: open ones expire by themselves after their validity; convert or cancel (with a reason). */
export function QuotationsList({ rows, today, onConvert, onCancel }: { rows: Quote[]; today: string; onConvert: (no: string) => void; onCancel: (no: string, reason: string) => void }) {
  const [cancelling, setCancelling] = React.useState<string | null>(null);
  const [reason, setReason] = React.useState("");
  const status = (q: Quote) => (q.status === "Open" && addDays(q.date, q.validDays) < today ? "Expired" : q.status);
  return (
    <ListTable
      rows={rows}
      rowKey={(r) => r.no}
      filters={[{ label: "Status", all: "Any status", value: status }]}
      search={{ placeholder: "Quotation or customer", text: (r) => `${r.no} ${r.customer}` }}
      cols={[
        { key: "no", header: "Quotation", cell: (r) => (<><span className="font-mono text-xs">{r.no}</span><span className="block text-xs text-muted-foreground">{formatDate(r.date)} · valid to {formatDate(addDays(r.date, r.validDays))}</span></>) },
        { key: "c", header: "Customer", cell: (r) => r.customer },
        { key: "t", header: "Total", numeric: true, cell: (r) => kes(r.total) },
        {
          key: "s",
          header: "Status",
          cell: (r) => {
            const s = status(r);
            return (
              <>
                <Tag tone={s === "Converted" ? "ok" : s === "Open" ? "neutral" : s === "Expired" ? "warn" : "bad"}>{s}</Tag>
                {r.convertedTo ? <span className="mt-0.5 block font-mono text-xs text-muted-foreground">→ {r.convertedTo}</span> : null}
                {r.cancelReason ? <span className="mt-0.5 block text-xs text-muted-foreground">{r.cancelReason}</span> : null}
              </>
            );
          },
        },
        {
          key: "act",
          header: "",
          cell: (r) =>
            status(r) === "Open" ? (
              cancelling === r.no ? (
                <span className="flex flex-wrap gap-1">
                  <Input aria-label="Why cancel" placeholder="Why?" className="h-8 w-40" value={reason} onChange={(e) => setReason(e.target.value)} />
                  <Button size="sm" variant="destructive" disabled={!reason.trim()} onClick={() => (onCancel(r.no, reason.trim()), setCancelling(null))}>
                    Cancel it
                  </Button>
                </span>
              ) : (
                <span className="flex gap-1">
                  <Button size="sm" variant="outline" onClick={() => onConvert(r.no)}>
                    Convert
                  </Button>
                  <Button size="sm" variant="ghost" onClick={() => (setCancelling(r.no), setReason(""))}>
                    Cancel
                  </Button>
                </span>
              )
            ) : status(r) === "Expired" ? (
              <Button size="sm" variant="ghost" onClick={() => onConvert(r.no)}>
                Re-quote
              </Button>
            ) : null,
        },
      ]}
    />
  );
}

/* ------------------------------------ RP-02 Stock as at a date ------------------------------------ */

export type StockRow = { product: string; unit: string; current: number; reorder: number; cost: number; price: number; movements: { date: string; qty: number }[]; nextExpiry?: string };

/**
 * Closing stock as at any date: today's stock rewound by every movement after that date (sales, receipts,
 * returns, adjustments). Valued at batch cost. A date before movement records began can't be rewound
 * reliably, so the report says so instead of showing a confident number.
 */
export function StockAsAt({ rows, today, recordsSince }: { rows: StockRow[]; today: string; recordsSince: string }) {
  const [asOf, setAsOf] = React.useState(today);
  const unreliable = asOf < recordsSince;
  const data = rows.map((r) => {
    const after = sum(r.movements.filter((m) => m.date > asOf), (m) => m.qty);
    const closing = r.current - after;
    return { ...r, closing, low: closing < r.reorder, expiring: !!r.nextExpiry && r.nextExpiry <= addDays(asOf, 90) };
  });
  return (
    <div className="space-y-3">
      <div className="flex flex-wrap items-end gap-3">
        <div className="space-y-1">
          <label htmlFor="sa-date" className="text-sm font-medium">
            Stock as at
          </label>
          <Input id="sa-date" type="date" className="w-44" max={today} value={asOf} onChange={(e) => setAsOf(e.target.value || today)} />
        </div>
        <p className="pb-2 text-sm">
          Value at cost: <strong className="tabular-nums">{kes(sum(data, (r) => Math.max(0, r.closing) * r.cost))}</strong> · at selling price {kes(sum(data, (r) => Math.max(0, r.closing) * r.price))}
        </p>
      </div>
      {unreliable ? <p className="rounded-md bg-status-warning-bg px-3 py-2 text-sm text-status-warning-text">Movement records start on {formatDate(recordsSince)}. Figures before then may be off, because earlier adjustments weren&apos;t recorded.</p> : null}
      <ListTable
        rows={data}
        rowKey={(r) => r.product}
        filters={[{ label: "Flag", all: "All items", value: (r) => (r.closing < 0 ? "Negative" : r.low ? "Low" : r.expiring ? "Expiring" : "OK") }]}
        cols={[
          { key: "p", header: "Product", cell: (r) => r.product },
          { key: "c", header: `Closing ${formatDate(asOf, { short: true })}`, numeric: true, cell: (r) => <span className={cn(r.closing < 0 && "text-status-error-text")}>{r.closing.toLocaleString("en-US")} {r.unit}</span> },
          { key: "n", header: "Now", numeric: true, cell: (r) => r.current.toLocaleString("en-US") },
          { key: "v", header: "Value at cost", numeric: true, cell: (r) => kes(Math.max(0, r.closing) * r.cost) },
          { key: "f", header: "Flags", cell: (r) => <span className="flex flex-wrap gap-1">{r.closing < 0 ? <Tag tone="bad">Negative: a movement is missing</Tag> : null}{r.low && r.closing >= 0 ? <Tag tone="warn">Below reorder</Tag> : null}{r.expiring ? <Tag tone="warn">Expiring within 90 days</Tag> : null}</span> },
        ]}
      />
    </div>
  );
}

/* ------------------------------------ RP-10 Supplier purchases ------------------------------------ */

export type PurchaseLine = { supplier: string; product: string; grn: string; qty: number; value: number };

/** Purchases by supplier or by product, with the weighted average cost (value ÷ units), linking to the GRNs. */
export function SupplierPurchases({ rows, onGrn }: { rows: PurchaseLine[]; onGrn: (grn: string) => void }) {
  const [by, setBy] = React.useState<"supplier" | "product">("supplier");
  const keyOf = (r: PurchaseLine) => (by === "supplier" ? r.supplier : r.product);
  const groups = [...new Set(rows.map(keyOf))].map((k) => {
    const rs = rows.filter((r) => keyOf(r) === k);
    return { k, rs, qty: sum(rs, (r) => r.qty), value: sum(rs, (r) => r.value), grns: [...new Set(rs.map((r) => r.grn))] };
  }).sort((a, b) => b.value - a.value);
  return (
    <div className="space-y-3">
      <div role="radiogroup" aria-label="Group by" className="inline-flex overflow-hidden rounded-md border border-[var(--color-border-panel)] text-sm bg-card">
        {(["supplier", "product"] as const).map((k) => (
          <button key={k} type="button" role="radio" aria-checked={by === k} onClick={() => setBy(k)} className={cn("cursor-pointer px-3 py-1.5", by === k ? "bg-[var(--color-bg-selected)] font-medium" : "text-muted-foreground hover:bg-muted")}>
            By {k}
          </button>
        ))}
      </div>
      <ListTable
        rows={groups}
        rowKey={(g) => g.k}
        expand={(g) => (
          <ul className="space-y-0.5 text-xs">
            {g.rs.map((r, i) => (
              <li key={i} className="flex justify-between gap-3">
                <span>
                  {by === "supplier" ? r.product : r.supplier} · {r.qty} at {kes(r.value / r.qty)}{" "}
                  <button type="button" className="cursor-pointer font-mono text-primary underline-offset-4 hover:underline" onClick={() => onGrn(r.grn)}>
                    {r.grn}
                  </button>
                </span>
                <span className="tabular-nums">{kes(r.value)}</span>
              </li>
            ))}
          </ul>
        )}
        cols={[
          { key: "k", header: by === "supplier" ? "Supplier" : "Product", cell: (g) => g.k },
          { key: "g", header: "GRNs", numeric: true, cell: (g) => g.grns.length },
          { key: "q", header: "Units", numeric: true, cell: (g) => g.qty.toLocaleString("en-US") },
          ...(by === "product" ? [{ key: "a", header: "Weighted average cost", numeric: true, cell: (g: (typeof groups)[number]) => kes(g.value / g.qty) }] : []),
          { key: "v", header: "Value (before VAT)", numeric: true, cell: (g) => kes(g.value), total: (s) => kes(sum(s, (r) => r.value)) },
        ]}
        totalsLabel={() => "All purchases"}
      />
    </div>
  );
}
