"use client";

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

import { CsvImportReview, downloadText, FileUpload, reviewCsv, type CsvReviewResult } from "@/components/medora/file-upload";
import { kes } from "@/components/medora/payroll";
import { SecretInput, type SecretStatus } from "@/components/medora/secret-input";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
import { formatDate } from "@/lib/format";
import { cn } from "@/lib/utils";

/*
 * Setup screens found missing in the last PRD pass: opening balances for customers and suppliers (AC-06),
 * leave types and allocations (HR-04), M-Pesa keys per branch (IG-03, ST-18) and price-group prices by CSV
 * (IN-03). Each is done once when a pharmacy starts on Medora, or rarely after, so each says plainly what
 * it will change before anything is saved.
 */

const isMoney = (s: string) => /^\d+(\.\d{1,2})?$/.test(s.replace(/,/g, ""));
const num = (s: string) => Number(s.replace(/,/g, "")) || 0;

/* ------------------------------------ AC-06 Opening balances for customers and suppliers ------------------------------------ */

export type OpeningParty = { id: number; name: string };
export type PartyOpening = { amount: string; ref: string; dated: string; due: string };

/**
 * What each customer owed us (or each supplier was owed) on the day Medora starts. Each balance becomes an
 * opening invoice or bill, so it ages like any other from its own date. The other side of every entry is
 * Opening balance equity, so the ledger balances while the old system's figures are brought across.
 */
export function PartyOpeningBalances({ kind, parties, asAt, lines, onChange }: { kind: "customer" | "supplier"; parties: OpeningParty[]; asAt: string; lines: Record<number, PartyOpening>; onChange: (id: number, l: PartyOpening) => void }) {
  const [csv, setCsv] = React.useState<CsvReviewResult | null>(null);
  const word = kind === "customer" ? "customer" : "supplier";
  const owes = kind === "customer" ? "Owed to us" : "We owe";
  const problems = (l?: PartyOpening) => {
    if (!l || !l.amount) return [];
    const p: string[] = [];
    if (!isMoney(l.amount)) p.push("Enter an amount.");
    if (!l.dated) p.push("Enter the invoice date, so it ages correctly.");
    else if (l.dated > asAt) p.push(`Must be on or before ${formatDate(asAt)}.`);
    if (l.due && l.dated && l.due < l.dated) p.push("Due before it was dated.");
    return p;
  };
  const total = Object.values(lines).reduce((s, l) => s + (isMoney(l.amount) ? num(l.amount) : 0), 0);
  const entered = Object.values(lines).filter((l) => l.amount).length;
  const control = kind === "customer" ? "1100 Accounts receivable" : "2000 Accounts payable";

  if (csv)
    return (
      <CsvImportReview
        result={csv}
        noun={`${word} balances`}
        onChooseAnother={() => setCsv(null)}
        onImport={() => {
          for (const r of csv.valid) {
            const p = parties.find((x) => x.name.toLowerCase() === r.name.trim().toLowerCase());
            if (p) onChange(p.id, { amount: r.amount, ref: r.ref ?? "", dated: r.dated, due: r.due ?? "" });
          }
          setCsv(null);
        }}
      />
    );

  return (
    <div className="space-y-3">
      <p className="text-sm text-muted-foreground">
        Balances as at <strong className="text-foreground">{formatDate(asAt)}</strong>, the day before Medora starts. Leave a {word} blank if the balance is zero.
      </p>
      <div className="overflow-x-auto rounded-md border border-[var(--color-border-panel)] bg-card">
        <table className="w-full min-w-[720px] text-sm">
          <thead className="bg-[var(--color-bg-surface-subtle)] text-left text-xs text-muted-foreground">
            <tr>
              <th className="px-2 py-2 font-medium">{kind === "customer" ? "Customer" : "Supplier"}</th>
              <th className="w-36 px-2 py-2 text-right font-medium">{owes}</th>
              <th className="w-36 px-2 py-2 font-medium">Their invoice no.</th>
              <th className="w-40 px-2 py-2 font-medium">Invoice date</th>
              <th className="w-40 px-2 py-2 font-medium">Due date</th>
            </tr>
          </thead>
          <tbody className="divide-y divide-[var(--color-border-subtle)]">
            {parties.map((p) => {
              const l = lines[p.id] ?? { amount: "", ref: "", dated: "", due: "" };
              const pr = problems(lines[p.id]);
              const set = (patch: Partial<PartyOpening>) => onChange(p.id, { ...l, ...patch });
              return (
                <tr key={p.id} className="align-top">
                  <td className="px-2 py-2">
                    {p.name}
                    {pr.length ? <span className="block text-xs text-status-error-text">{pr.join(" ")}</span> : null}
                  </td>
                  <td className="px-2 py-1.5">
                    <Input aria-label={`${p.name} amount`} className="text-right tabular-nums" inputMode="decimal" value={l.amount} onChange={(e) => set({ amount: e.target.value })} aria-invalid={pr.length > 0} />
                  </td>
                  <td className="px-2 py-1.5">
                    <Input aria-label={`${p.name} invoice number`} value={l.ref} onChange={(e) => set({ ref: e.target.value })} disabled={!l.amount} />
                  </td>
                  <td className="px-2 py-1.5">
                    <Input aria-label={`${p.name} invoice date`} type="date" max={asAt} value={l.dated} onChange={(e) => set({ dated: e.target.value })} disabled={!l.amount} />
                  </td>
                  <td className="px-2 py-1.5">
                    <Input aria-label={`${p.name} due date`} type="date" value={l.due} onChange={(e) => set({ due: e.target.value })} disabled={!l.amount} />
                  </td>
                </tr>
              );
            })}
          </tbody>
          <tfoot className="border-t-2 border-[var(--color-border-panel)] bg-muted/30 font-semibold">
            <tr>
              <td className="px-2 py-2">
                {entered} of {parties.length} {word}s with a balance
              </td>
              <td className="px-2 py-2 text-right tabular-nums">{kes(total)}</td>
              <td colSpan={3} />
            </tr>
          </tfoot>
        </table>
      </div>
      <div className="flex flex-wrap items-start justify-between gap-3">
        <div className="w-full max-w-md">
          <FileUpload
            label={`Import ${word} balances (CSV)`}
            hint="Columns: Name, Amount, Invoice no., Invoice date, Due date. Names must match existing records."
            kind="csv"
            accept={[".csv"]}
            maxBytes={2 * 1024 * 1024}
            compact
            onTemplate={() => downloadText(`${word}-opening-balances.csv`, `Name,Amount,Invoice no.,Invoice date,Due date\n${parties[0]?.name ?? ""},0.00,,${asAt},\n`)}
            upload={async (file, prog) => {
              prog(100);
              const text = await file.text();
              setCsv(
                reviewCsv(
                  text,
                  [
                    { key: "name", header: "Name", required: true, validate: (v) => (parties.some((p) => p.name.toLowerCase() === v.trim().toLowerCase()) ? undefined : `No ${word} with this name. Add them first.`) },
                    { key: "amount", header: "Amount", required: true, validate: (v) => (isMoney(v) ? undefined : "Not an amount.") },
                    { key: "ref", header: "Invoice no." },
                    { key: "dated", header: "Invoice date", required: true, validate: (v) => (/^\d{4}-\d{2}-\d{2}$/.test(v.trim()) ? (v.trim() > asAt ? `After ${asAt}.` : undefined) : "Use YYYY-MM-DD.") },
                    { key: "due", header: "Due date" },
                  ],
                  "name",
                ),
              );
            }}
          />
        </div>
        <div className="rounded-md border border-[var(--color-border-panel)] bg-muted/30 p-3 text-sm">
          <p className="mb-1 text-xs font-semibold text-muted-foreground uppercase">Will post to the ledger</p>
          <p className="tabular-nums">
            {kind === "customer" ? `Debit ${control}` : "Debit 3900 Opening balance equity"} {kes(total)}
          </p>
          <p className="tabular-nums">
            {kind === "customer" ? "Credit 3900 Opening balance equity" : `Credit ${control}`} {kes(total)}
          </p>
        </div>
      </div>
    </div>
  );
}

/* ------------------------------------ HR-04 Leave types and allocations ------------------------------------ */

export type LeaveType = { id: string; name: string; days: string; carryMax: string; paid: boolean; document: boolean; appliesTo: "All staff" | "Permanent only" | "Women" | "Men" };

export function leaveTypeProblems(t: LeaveType, all: LeaveType[]) {
  const p: string[] = [];
  if (!t.name.trim()) p.push("Name the leave type.");
  else if (all.some((x) => x.id !== t.id && x.name.trim().toLowerCase() === t.name.trim().toLowerCase())) p.push("Another leave type has this name.");
  if (!/^\d+$/.test(t.days) || Number(t.days) > 366) p.push("Days per year: a whole number, 0 to 366.");
  if (t.carryMax && (!/^\d+$/.test(t.carryMax) || Number(t.carryMax) > Number(t.days))) p.push("Carry-over can't be more than the yearly days.");
  return p;
}

/**
 * Leave types with their yearly allocation, how much rolls over into next year, and who they apply to. The
 * preview shows what one employee gets this year, so a change is checked against a real person.
 */
export function LeaveTypes({ types, onChange, preview }: { types: LeaveType[]; onChange: (t: LeaveType[]) => void; preview: { name: string; kind: "Permanent" | "Contract" | "Casual"; gender: "Woman" | "Man"; leftLastYear: Record<string, number> } }) {
  const set = (id: string, patch: Partial<LeaveType>) => onChange(types.map((t) => (t.id === id ? { ...t, ...patch } : t)));
  const applies = (t: LeaveType) => t.appliesTo === "All staff" || (t.appliesTo === "Permanent only" && preview.kind === "Permanent") || (t.appliesTo === "Women" && preview.gender === "Woman") || (t.appliesTo === "Men" && preview.gender === "Man");
  return (
    <div className="space-y-4">
      <div className="overflow-x-auto rounded-md border border-[var(--color-border-panel)] bg-card">
        <table className="w-full min-w-[760px] text-sm">
          <thead className="bg-[var(--color-bg-surface-subtle)] text-left text-xs text-muted-foreground">
            <tr>
              <th className="px-2 py-2 font-medium">Leave type</th>
              <th className="w-24 px-2 py-2 text-right font-medium">Days a year</th>
              <th className="w-28 px-2 py-2 text-right font-medium">Carry over, max</th>
              <th className="w-40 px-2 py-2 font-medium">Applies to</th>
              <th className="w-20 px-2 py-2 font-medium">Paid</th>
              <th className="w-28 px-2 py-2 font-medium">Needs a document</th>
              <th className="w-10" />
            </tr>
          </thead>
          <tbody className="divide-y divide-[var(--color-border-subtle)]">
            {types.map((t) => {
              const pr = leaveTypeProblems(t, types);
              return (
                <tr key={t.id} className="align-top">
                  <td className="px-2 py-1.5">
                    <Input aria-label="Leave type name" value={t.name} onChange={(e) => set(t.id, { name: e.target.value })} aria-invalid={pr.length > 0} />
                    {pr.length ? <span className="mt-0.5 block text-xs text-status-error-text">{pr.join(" ")}</span> : null}
                  </td>
                  <td className="px-2 py-1.5">
                    <Input aria-label={`${t.name} days a year`} className="text-right tabular-nums" inputMode="numeric" value={t.days} onChange={(e) => set(t.id, { days: e.target.value })} />
                  </td>
                  <td className="px-2 py-1.5">
                    <Input aria-label={`${t.name} carry over`} className="text-right tabular-nums" inputMode="numeric" placeholder="None" value={t.carryMax} onChange={(e) => set(t.id, { carryMax: e.target.value })} />
                  </td>
                  <td className="px-2 py-1.5">
                    <NativeSelect aria-label={`${t.name} applies to`} value={t.appliesTo} onChange={(e) => set(t.id, { appliesTo: e.target.value as LeaveType["appliesTo"] })}>
                      {(["All staff", "Permanent only", "Women", "Men"] as const).map((o) => (
                        <NativeSelectOption key={o}>{o}</NativeSelectOption>
                      ))}
                    </NativeSelect>
                  </td>
                  <td className="px-2 py-2.5">
                    <Checkbox aria-label={`${t.name} is paid`} checked={t.paid} onCheckedChange={(v) => set(t.id, { paid: !!v })} />
                  </td>
                  <td className="px-2 py-2.5">
                    <Checkbox aria-label={`${t.name} needs a document`} checked={t.document} onCheckedChange={(v) => set(t.id, { document: !!v })} />
                  </td>
                  <td className="px-1 py-1.5">
                    <Button size="icon-sm" variant="ghost" aria-label={`Remove ${t.name}`} onClick={() => onChange(types.filter((x) => x.id !== t.id))}>
                      <Trash2Icon />
                    </Button>
                  </td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
      <Button size="sm" variant="outline" onClick={() => onChange([...types, { id: `t${Date.now()}`, name: "", days: "0", carryMax: "", paid: true, document: false, appliesTo: "All staff" }])}>
        <PlusIcon data-icon="inline-start" />
        Add leave type
      </Button>
      <div className="rounded-md border border-[var(--color-border-panel)] p-3 bg-card">
        <p className="text-sm font-medium">
          This year for {preview.name} ({preview.kind.toLowerCase()})
        </p>
        <ul className="mt-1 grid gap-1 text-sm sm:grid-cols-2">
          {types.filter((t) => t.name.trim()).map((t) => {
            if (!applies(t)) return (<li key={t.id} className="text-muted-foreground">{t.name}: not applicable</li>);
            const carried = Math.min(preview.leftLastYear[t.name] ?? 0, Number(t.carryMax) || 0);
            return (
              <li key={t.id} className="tabular-nums">
                {t.name}: <strong>{Number(t.days) + carried} days</strong>
                {carried ? <span className="text-muted-foreground"> ({t.days} + {carried} carried over)</span> : null}
                {!t.paid ? <span className="text-muted-foreground"> · unpaid</span> : null}
              </li>
            );
          })}
        </ul>
      </div>
    </div>
  );
}

/* ------------------------------------ IG-03, ST-18 M-Pesa keys per branch ------------------------------------ */

export type MpesaBranch = { branch: string; type: "Paybill" | "Till (Buy Goods)"; shortcode: string; till: string; keys: SecretStatus; passkey: SecretStatus; lastTest?: { ok: boolean; at: string; note?: string } };

export function mpesaBranchProblems(b: MpesaBranch) {
  const p: string[] = [];
  if (!/^\d{5,7}$/.test(b.shortcode)) p.push("Shortcode is 5 to 7 digits, from the Daraja portal.");
  if (b.type === "Till (Buy Goods)" && !/^\d{5,7}$/.test(b.till)) p.push("Enter the till number customers pay to.");
  if (!b.keys.set) p.push("Consumer key and secret are not set.");
  if (!b.passkey.set) p.push("Passkey is not set.");
  return p;
}

/**
 * M-Pesa credentials per branch. Each branch has its own shortcode and keys, so one branch's money never
 * lands in another's till. Secrets are write-only: replaced, never shown. The callback URL comes from the
 * server's configuration and is shown so it can be registered on Daraja, not edited here.
 *
 * `onSave` (card 008): a live page receives the branch and the newly typed secrets, persists them with
 * the branch's shortcode, type and till, and throws to keep them in the form. With `onSave`, Save is
 * always offered, so a shortcode or till change can be saved on its own. Without it (the demo), Save
 * appears only while new secrets are typed, as before.
 */
export type MpesaSecrets = { consumerKey?: string; consumerSecret?: string; passkey?: string };

export function MpesaBranchKeys({ branches, onChange, callbackUrl, environment, onTest, onSave }: { branches: MpesaBranch[]; onChange: (b: MpesaBranch[]) => void; callbackUrl: string; environment: "Sandbox" | "Live"; onTest: (branch: string) => void; onSave?: (branch: string, secrets: MpesaSecrets) => Promise<void> | void }) {
  const [open, setOpen] = React.useState(branches[0]?.branch ?? "");
  // New secrets are held here while typed and only marked set on Save; remounting (`gen`) clears the fields.
  const [pending, setPending] = React.useState<Record<string, { keys?: Record<string, string>; passkey?: Record<string, string> }>>({});
  const [saving, setSaving] = React.useState("");
  const [gen, setGen] = React.useState(0);
  // R11 (card 037): what a branch still needs is listed once someone tries to save or test it, not on arrival.
  const [tried, setTried] = React.useState<string[]>([]);
  const attempt = (name: string) => setTried((t) => (t.includes(name) ? t : [...t, name]));
  const set = (name: string, patch: Partial<MpesaBranch>) => onChange(branches.map((b) => (b.branch === name ? { ...b, ...patch } : b)));
  const hold = (name: string, which: "keys" | "passkey", v: Record<string, string> | null) =>
    setPending((p) => ({ ...p, [name]: { ...p[name], [which]: v && Object.values(v).every((x) => x.trim()) ? v : undefined } }));
  const save = async (name: string) => {
    attempt(name);
    const p = pending[name] ?? {};
    if (onSave) {
      setSaving(name);
      try {
        await onSave(name, { consumerKey: p.keys?.key, consumerSecret: p.keys?.secret, passkey: p.passkey?.passkey });
      } catch {
        return; // the page says why; the typed secrets stay in the form
      } finally {
        setSaving("");
      }
    }
    const stamp = { set: true, lastChanged: { at: "just now", by: "you" } };
    set(name, { ...(p.keys ? { keys: stamp } : {}), ...(p.passkey ? { passkey: stamp } : {}), lastTest: undefined });
    setPending((x) => ({ ...x, [name]: {} }));
    setGen((g) => g + 1);
  };
  return (
    <div className="space-y-3">
      <div className="rounded-md border border-[var(--color-border-panel)] bg-muted/30 px-3 py-2 text-sm">
        <span className="text-muted-foreground">Callback URL ({environment}), set by the server:</span>{" "}
        <code className="font-mono text-xs break-all">{callbackUrl}</code>
        <span className="block text-xs text-muted-foreground">Register this on Daraja for every shortcode below. It can&apos;t be changed here.</span>
      </div>
      <ul className="divide-y divide-[var(--color-border-subtle)] rounded-md border border-[var(--color-border-panel)] bg-card">
        {branches.map((b) => {
          const pr = mpesaBranchProblems(b);
          const isOpen = open === b.branch;
          return (
            <li key={b.branch}>
              <button type="button" aria-expanded={isOpen} onClick={() => setOpen(isOpen ? "" : b.branch)} className="flex w-full cursor-pointer flex-wrap items-center gap-x-3 gap-y-1 px-3 py-2 text-left text-sm hover:bg-muted/40">
                <span className="font-medium">{b.branch}</span>
                <span className="text-xs text-muted-foreground tabular-nums">
                  {b.type} · {b.shortcode || "no shortcode"}
                </span>
                <span className={cn("ml-auto rounded px-1.5 py-0.5 text-[11px] font-medium", pr.length ? "bg-status-warning-bg text-status-warning-text" : b.lastTest && !b.lastTest.ok ? "bg-status-error-bg text-status-error-text" : "bg-status-success-bg text-status-success-text")}>
                  {pr.length ? `${pr.length} to set up` : b.lastTest && !b.lastTest.ok ? "Test failed" : "Ready"}
                </span>
              </button>
              {isOpen ? (
                <div className="space-y-3 border-t border-[var(--color-border-subtle)] bg-muted/20 px-3 py-3">
                  <div className="grid gap-3 sm:grid-cols-3">
                    <div className="space-y-1">
                      <label htmlFor={`mp-type-${b.branch}`} className="text-sm font-medium">
                        Account type
                      </label>
                      <NativeSelect id={`mp-type-${b.branch}`} value={b.type} onChange={(e) => set(b.branch, { type: e.target.value as MpesaBranch["type"] })}>
                        <NativeSelectOption>Paybill</NativeSelectOption>
                        <NativeSelectOption>Till (Buy Goods)</NativeSelectOption>
                      </NativeSelect>
                    </div>
                    <div className="space-y-1">
                      <label htmlFor={`mp-sc-${b.branch}`} className="text-sm font-medium">
                        Shortcode
                      </label>
                      <Input id={`mp-sc-${b.branch}`} className="font-mono" inputMode="numeric" value={b.shortcode} onChange={(e) => set(b.branch, { shortcode: e.target.value.replace(/\D/g, "") })} />
                    </div>
                    {b.type === "Till (Buy Goods)" ? (
                      <div className="space-y-1">
                        <label htmlFor={`mp-till-${b.branch}`} className="text-sm font-medium">
                          Till number
                        </label>
                        <Input id={`mp-till-${b.branch}`} className="font-mono" inputMode="numeric" value={b.till} onChange={(e) => set(b.branch, { till: e.target.value.replace(/\D/g, "") })} />
                      </div>
                    ) : null}
                  </div>
                  <div className="grid gap-3 lg:grid-cols-2">
                    <div className="space-y-1">
                      <p className="text-sm font-medium">Consumer key and secret</p>
                      <SecretInput key={`k${gen}`} fields={[{ name: "key", label: "Consumer key" }, { name: "secret", label: "Consumer secret" }]} status={b.keys} required hint="From the app on the Daraja portal. They won't be shown again after saving." onReplace={(v) => hold(b.branch, "keys", v)} />
                    </div>
                    <div className="space-y-1">
                      <p className="text-sm font-medium">Passkey</p>
                      <SecretInput key={`p${gen}`} fields={[{ name: "passkey", label: "Passkey" }]} status={b.passkey} required hint="The STK push passkey Safaricom sent for this shortcode." onReplace={(v) => hold(b.branch, "passkey", v)} />
                    </div>
                  </div>
                  {pr.length && tried.includes(b.branch) ? (
                    <ul className="list-disc pl-5 text-sm text-status-warning-text">
                      {pr.map((x) => (
                        <li key={x}>{x}</li>
                      ))}
                    </ul>
                  ) : null}
                  <div className="flex flex-wrap items-center gap-3">
                    {onSave || pending[b.branch]?.keys || pending[b.branch]?.passkey ? (
                      <Button size="sm" disabled={saving === b.branch} onClick={() => void save(b.branch)}>
                        {onSave ? `Save ${b.branch}` : `Save ${b.branch} keys`}
                      </Button>
                    ) : null}
                    <Button size="sm" variant="outline" onClick={() => (pr.length ? attempt(b.branch) : onTest(b.branch))}>
                      Test {b.branch}
                    </Button>
                    {b.lastTest ? <span className={cn("text-xs", b.lastTest.ok ? "text-status-success-text" : "text-status-error-text")}>{b.lastTest.ok ? `Connected, ${b.lastTest.at}` : `Failed ${b.lastTest.at}: ${b.lastTest.note}`}</span> : null}
                  </div>
                </div>
              ) : null}
            </li>
          );
        })}
      </ul>
    </div>
  );
}

/* ------------------------------------ IN-03 Price-group prices by CSV ------------------------------------ */

/** Download the current price-group prices as a CSV, edit them, and upload: checked row by row first. */
export function PriceGroupCsv({ products, groups, prices, onApply }: { products: { id: number; code: string; name: string; cost: number }[]; groups: string[]; prices: Record<string, string>; onApply: (updates: Record<string, string>) => void }) {
  const [csv, setCsv] = React.useState<CsvReviewResult | null>(null);
  const exportCsv = () => {
    const rows = products.flatMap((p) => groups.map((g) => [p.code, p.name.replace(/,/g, " "), g, prices[`${p.id}:${g}`] ?? ""].join(",")));
    downloadText("price-group-prices.csv", ["Item code,Product,Group,Price", ...rows].join("\n"));
  };
  if (csv)
    return (
      <CsvImportReview
        result={csv}
        noun="prices"
        onChooseAnother={() => setCsv(null)}
        onImport={() => {
          const u: Record<string, string> = {};
          for (const r of csv.valid) {
            const p = products.find((x) => x.code === r.code.trim().toUpperCase());
            if (p) u[`${p.id}:${r.group.trim()}`] = r.price.trim();
          }
          onApply(u);
          setCsv(null);
        }}
      />
    );
  return (
    <div className="flex flex-wrap items-start gap-3">
      <Button size="sm" variant="outline" onClick={exportCsv}>
        Download current prices (CSV)
      </Button>
      <div className="min-w-64 flex-1">
        <FileUpload
          label="Upload edited prices"
          hint="Same columns as the download. A blank price removes the group price, so the group pays retail."
          kind="csv"
          accept={[".csv"]}
          maxBytes={2 * 1024 * 1024}
          compact
          upload={async (file, prog) => {
            prog(100);
            const text = await file.text();
            setCsv(
              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: "group", header: "Group", required: true, validate: (v) => (groups.includes(v.trim()) ? undefined : `Not a price group. Use ${groups.join(", ")}.`) },
                { key: "price", header: "Price", validate: (v) => (!v.trim() || isMoney(v) ? undefined : "Not an amount.") },
              ]),
            );
          }}
        />
      </div>
    </div>
  );
}

/* ------------------------------------ IG-18 Bank connections ------------------------------------ */

export type BankLink = { bank: string; account: string; ledger: string; api: boolean; connected: boolean; lastSync?: string; error?: string };

/**
 * Direct bank connections, per bank and account. Where a bank offers an API the account connects and its
 * statement lines arrive by themselves for reconciliation (AC-12); where it doesn't, the row says so and the
 * statement is imported as a CSV instead (IG-17). Connecting hands over to the bank's own sign-in, so no
 * banking password is ever typed into Medora.
 */
export function BankConnections({ links, onConnect, onDisconnect, onImport }: { links: BankLink[]; onConnect: (account: string) => void; onDisconnect: (account: string) => void; onImport: (account: string) => void }) {
  return (
    <ul className="divide-y divide-[var(--color-border-subtle)] rounded-md border border-[var(--color-border-panel)] bg-card">
      {links.map((l) => (
        <li key={l.account} className="flex flex-wrap items-center gap-x-4 gap-y-2 px-3 py-2.5 text-sm">
          <span className="min-w-48 flex-1">
            <span className="font-medium">{l.bank}</span> <span className="font-mono text-xs text-muted-foreground">{l.account}</span>
            <span className="block text-xs text-muted-foreground">Ledger account {l.ledger}</span>
          </span>
          <span className="min-w-52 text-xs">
            {!l.api ? (
              <span className="text-muted-foreground">No API from this bank: import statements as CSV</span>
            ) : l.error ? (
              <span className="font-medium text-status-error-text">Connection failed: {l.error}</span>
            ) : l.connected ? (
              <span className="text-status-success-text">Connected · last synced {l.lastSync}</span>
            ) : (
              <span className="text-muted-foreground">Not connected</span>
            )}
          </span>
          <span className="flex gap-1">
            {l.api ? (
              l.connected && !l.error ? (
                <Button size="sm" variant="ghost" onClick={() => onDisconnect(l.account)}>
                  Disconnect
                </Button>
              ) : (
                <Button size="sm" variant="outline" onClick={() => onConnect(l.account)}>
                  {l.error ? "Reconnect" : "Connect"}
                </Button>
              )
            ) : (
              <Button size="sm" variant="outline" onClick={() => onImport(l.account)}>
                Import statement
              </Button>
            )}
          </span>
        </li>
      ))}
    </ul>
  );
}
