/* ============================================================
   admin.jsx — Lekha admin (internal operator)
   Cross-business console · manual-approval gate · global toggle.
   The gate lets the admin VIEW + EDIT the OCR-parsed debit-note
   fields, then Approve or Reject (per Q10).
   ============================================================ */

function AdminApp({ store }) {
  const { docs, businesses, bizById, setBizGate, setBizFinanceGate, adminApprove, adminReject } = store;
  const [tab, setTab] = React.useState("overview");
  const [bizFilter, setBizFilter] = React.useState("all");
  const [statusFilter, setStatusFilter] = React.useState("all");
  const [openId, setOpenId] = React.useState(null);
  const [gateId, setGateId] = React.useState(null);

  // OCR gate = raw debit notes awaiting admin verify (pre-compose); Draft review
  // (G1) = composed credit notes in 'draft'. New-schema statuses (2026-07 remaster).
  const gateQ = docs.filter((d) => ["received", "validated", "validation_failed"].includes(d.status));
  const draftQ = docs.filter((d) => d.status === "draft");
  const open = openId && docs.find((d) => d.id === openId);
  const gateDoc = gateId && docs.find((d) => d.id === gateId);

  const who = store.user || {};
  const gateOn = businesses.filter((b) => b.manual_approval).length;
  const tallyQueued = (store.bridges || []).reduce((n, b) => n + (b.queued || 0), 0);
  const [excCount, setExcCount] = React.useState(0);
  React.useEffect(() => { api.get("/api/exceptions?status=OPEN").then((d) => setExcCount(d.total)).catch(() => {}); }, [docs.length]);
  const tabs = [
    { id: "overview", label: "Overview" },
    { id: "queue", label: "Approval Queue", count: gateQ.length },
    { id: "drafts", label: "Draft Review", count: draftQ.length || null },
    { id: "all", label: "All Documents", count: docs.length },
    { id: "exceptions", label: "Exceptions", count: excCount || null },
    { id: "loops", label: "Feedback Loops" },
    { id: "replica", label: "Tally Sync" },
    { id: "settings", label: "Settings" },
  ];

  return (
    <div className="app">
      <AppHeader
        store={store}
        sub="Operations Console"
        who={{ av: (who.name || "LA").slice(0, 2).toUpperCase(), name: who.name || "Lekha Admin", role: "Internal operator · all tenants" }}
        right={
          <div className="row gap10" style={{ color: "#cfcdc6", fontSize: 12.5, marginRight: 4 }}>
            <span className="row gap8" style={{ background: "#1c1c1d", border: "1px solid #2a2a2b", padding: "7px 12px", borderRadius: 999 }}>
              <span className="bizdot" style={{ background: gateOn ? "var(--lime)" : "#6b6a61" }} />
              Verify gate on for {gateOn}/{businesses.length}
            </span>
          </div>
        }
      />
      <Tabs items={tabs} active={tab} onChange={setTab} />
      <div className="main">
        <div className="container">
          {tab === "overview" && <AdminOverview store={store} onGoQueue={() => setTab("queue")} />}
          {tab === "queue" && <AdminQueue docs={gateQ} bizById={bizById} approve={adminApprove} reject={adminReject} onOpen={setGateId} />}
          {tab === "drafts" && <AdminDrafts docs={draftQ} bizById={bizById} approve={store.draftApprove} reject={store.draftReject} onOpen={setOpenId} />}
          {tab === "all" && (
            <AdminAll
              docs={docs} businesses={businesses} bizById={bizById}
              bizFilter={bizFilter} setBizFilter={setBizFilter}
              statusFilter={statusFilter} setStatusFilter={setStatusFilter} onOpen={setOpenId}
            />
          )}
          {tab === "exceptions" && <AdminExceptions store={store} />}
          {tab === "loops" && <AdminFeedbackLoops store={store} onGoExceptions={() => setTab("exceptions")} />}
          {tab === "replica" && <AdminReplica store={store} />}
          {tab === "settings" && <AdminSettings businesses={businesses} setBizGate={setBizGate} setBizFinanceGate={setBizFinanceGate} pending={gateQ.length} store={store} />}
        </div>
      </div>
      {open && <NakadDrawer doc={open} biz={bizById(open.biz)} admin onClose={() => setOpenId(null)} />}
      {gateDoc && <AdminGateDrawer doc={gateDoc} biz={bizById(gateDoc.biz)} store={store} onClose={() => setGateId(null)} />}
    </div>
  );
}

/* ---- overview ------------------------------------------------------------ */
function AdminOverview({ store, onGoQueue }) {
  const { docs, businesses } = store;
  const [conn, setConn] = React.useState(null);  // real connector health
  React.useEffect(() => { api.get("/api/admin/overview").then((d) => setConn(d.connectors)).catch(() => {}); }, [docs.length]);
  const count = (fn) => docs.filter(fn).length;
  const gate = count((d) => ["received", "validated", "validation_failed"].includes(d.status));
  const review = count((d) => ["draft", "pending_approval"].includes(d.status));
  const inflight = count((d) => ["approved", "irn_pending", "irn_generated", "irn_failed", "posting", "posted", "post_failed"].includes(d.status));
  const confirmed = count((d) => ["verified_in_tally", "closed"].includes(d.status));
  const rejected = count((d) => d.status === "cancelled");
  const gateOn = businesses.filter((b) => b.manual_approval).length;

  const tiles = [
    { label: "At Lekha admin gate", val: gate, foot: gate ? "Awaiting your approval" : "Queue clear", ic: <Ic.shield />, bg: "var(--amber-bg)", fg: "var(--amber-fg)", action: gate ? onGoQueue : null },
    { label: "In finance review", val: review, foot: "Awaiting finance teams", ic: <Ic.mail />, bg: "var(--blue-bg)", fg: "var(--blue-fg)" },
    { label: "Posting / in-flight", val: inflight, foot: "Moving to Tally", ic: <Ic.tally />, bg: "var(--teal-bg)", fg: "var(--teal-fg)" },
    { label: "Confirmed", val: confirmed, foot: rejected + " rejected", ic: <Ic.check />, bg: "var(--green-bg)", fg: "var(--green-fg)" },
  ];

  return (
    <>
      <div className="page-head"><div><div className="ph-title">Operations overview</div><div className="ph-sub">Every credit note across all {businesses.length} businesses, end to end.</div></div></div>

      <div className="kpis">
        {tiles.map((t, i) => (
          <div className="kpi" key={i} style={{ cursor: t.action ? "pointer" : "default" }} onClick={t.action || undefined}>
            <div className="k-top">
              <span className="k-label">{t.label}</span>
              <span className="k-ic" style={{ background: t.bg, color: t.fg }}>{t.ic}</span>
            </div>
            <div className="k-val">{t.val}</div>
            <div className="k-foot">{t.foot}{t.action ? " →" : ""}</div>
          </div>
        ))}
      </div>

      <div className="row gap16" style={{ alignItems: "stretch" }}>
        <div className="card grow" style={{ minWidth: 0 }}>
          <div className="card-pad" style={{ paddingBottom: 6 }}><div className="section-title" style={{ margin: 0 }}>By business</div></div>
          <table className="tbl">
            <thead><tr><th>Business</th><th>In pipeline</th><th>Confirmed</th><th>Approver</th></tr></thead>
            <tbody>
              {businesses.map((b) => {
                const ds = docs.filter((d) => d.biz === b.id);
                const inP = ds.filter((d) => !["verified_in_tally", "closed", "cancelled"].includes(d.status)).length;
                const conf = ds.filter((d) => ["verified_in_tally", "closed"].includes(d.status)).length;
                return (
                  <tr key={b.id} style={{ cursor: "default" }}>
                    <td><span className="row gap8"><BizAvatar biz={b} size={24} />{b.name}</span></td>
                    <td className="amt">{inP}</td>
                    <td className="amt">{conf}</td>
                    <td>{b.email ? <span className="badge green" style={{ fontSize: 10.5 }}><span className="b-dot" />Set</span> : <span className="badge amber" style={{ fontSize: 10.5 }}><span className="b-dot" />Missing</span>}</td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>

        <div className="card card-pad" style={{ flex: "0 0 320px" }}>
          <div className="section-title">Lekha admin gate</div>
          <div className="row gap12 mb16" style={{ alignItems: "flex-start" }}>
            <span className="k-ic" style={{ background: gateOn ? "var(--green-bg)" : "var(--gray-bg)", color: gateOn ? "var(--green-fg)" : "var(--gray-fg)", flex: "0 0 30px" }}><Ic.shield /></span>
            <div>
              <div style={{ fontWeight: 700 }}>Verify gate on for {gateOn} of {businesses.length}</div>
              <div className="tiny mt8" style={{ marginTop: 4 }}>Set per company in Settings — on for new brands, off once their debit notes are reliably clean.</div>
            </div>
          </div>
          {gate > 0 && (
            <button className="btn btn-primary" style={{ width: "100%" }} onClick={onGoQueue}><Ic.shield />Review {gate} waiting</button>
          )}
          <div className="divider" />
          <div className="section-title">Tally status</div>
          <div className="stack gap10">
            <ConnLine label="Tally (desktop app)" ok={conn ? conn.tally : null} />
            <ConnLine label="Debit note source" ok={conn ? conn.debit_api : null} />
            <ConnLine label="Mail delivery (Resend)" ok={conn ? conn.smtp : null} />
          </div>
        </div>
      </div>
    </>
  );
}
function ConnLine({ label, ok }) {
  const loading = ok == null;
  const color = loading ? "var(--text-3)" : ok ? "var(--green-fg)" : "var(--red-fg)";
  return (
    <div className="row gap10" style={{ justifyContent: "space-between" }}>
      <span className="row gap8" style={{ fontSize: 13.5 }}><span className="bizdot" style={{ background: color }} />{label}</span>
      <span className="tiny" style={{ color, fontWeight: 600 }}>{loading ? "Checking…" : ok ? "Healthy" : "Down"}</span>
    </div>
  );
}

/* ---- approval queue ------------------------------------------------------ */
function AdminQueue({ docs, bizById, approve, reject, onOpen }) {
  if (!docs.length) return <EmptyState title="Queue is clear" sub="No fetched debit notes are waiting for approval. Only companies with the verify gate on appear here." icon={<Ic.check />} />;
  return (
    <>
      <div className="page-head"><div><div className="ph-title">Approval Queue</div><div className="ph-sub">Verify the scanned details against the source PDF, edit if wrong, then approve to let Lekha generate the credit note.</div></div></div>
      <div className="hint mb20"><Ic.shield style={{ marginTop: 1, flex: "0 0 15px" }} /><div>Only debit notes from companies with the <b>verify gate on</b> wait here. Open one to check its scanned fields against the source document, correct anything wrong, then approve.</div></div>
      {(() => {
        // group the queue by company
        const groups = {};
        for (const d of docs) (groups[d.biz] ||= []).push(d);
        const order = Object.keys(groups).sort((a, b) =>
          (bizById(a).name || "").localeCompare(bizById(b).name || ""));
        return order.map((bid) => {
          const biz = bizById(bid);
          return (
            <div key={bid} className="mb20">
              <div className="row gap10" style={{ alignItems: "center", margin: "2px 2px 10px" }}>
                <BizAvatar biz={biz} size={28} />
                <span style={{ fontWeight: 800, fontSize: 15 }}>{biz.name}</span>
                <span className="tiny" style={{ background: "var(--bg)", border: "1px solid var(--border)", borderRadius: 999, padding: "1px 9px", fontWeight: 700, color: "var(--text-2)" }}>{groups[bid].length}</span>
              </div>
              <div className="stack gap12">
                {groups[bid].map((d) => (
                  <div className="card" key={d.id} style={{ padding: "16px 22px", display: "flex", alignItems: "center", gap: 18 }}>
                    <div className="grow" style={{ minWidth: 0, cursor: "pointer" }} onClick={() => onOpen(d.id)}>
                      <div className="row gap10" style={{ marginBottom: 4 }}><span style={{ fontWeight: 700, fontSize: 15 }} className="mono">{d.dnNo}</span></div>
                      <div className="tiny">Against {d.invoice} · {d.lines.length} line items · fetched {d.fetched}</div>
                    </div>
                    <div style={{ width: 120 }}><div className="tiny mb8" style={{ marginBottom: 4 }}>Confidence</div><Confidence value={d.confidence} /></div>
                    <div style={{ textAlign: "right", minWidth: 96 }}><div style={{ fontWeight: 700, fontSize: 17, fontVariantNumeric: "tabular-nums" }}>{fmtINR(d.amount)}</div></div>
                    <div className="row gap8">
                      <button className="btn btn-sm" onClick={() => onOpen(d.id)}>Verify</button>
                      <button className="btn btn-danger btn-sm" onClick={() => reject(d.id)}><Ic.x />Reject</button>
                      <button className="btn btn-primary btn-sm" onClick={() => approve(d.id)}><Ic.check />Approve</button>
                    </div>
                  </div>
                ))}
              </div>
            </div>
          );
        });
      })()}
    </>
  );
}

/* ---- G1: Lekha review of composed drafts (flow inversion 2026-07-05) ------
   The draft was composed on ACTUALS (real ledgers from the company's own
   chart) before anything became brand-visible. Open shows the same accounting
   impact the finance screen renders; approve releases it to brand finance. */
function DraftRow({ d, biz, approve, reject, onOpen }) {
  const [rejecting, setRejecting] = React.useState(false);
  const [reason, setReason] = React.useState("");
  const [working, setWorking] = React.useState(false);
  return (
    <div className="card" style={{ padding: "16px 22px" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 18 }}>
        <BizAvatar biz={biz} size={28} />
        <div className="grow" style={{ minWidth: 0, cursor: "pointer" }} onClick={() => onOpen(d.id)}>
          <div className="row gap10" style={{ marginBottom: 4 }}>
            <span style={{ fontWeight: 700, fontSize: 15 }} className="mono">{d.cnNo}</span>
            <span className="tiny">from {d.dnNo}</span>
          </div>
          <div className="tiny">{biz.name} · against {d.invoice} · {d.lines.length} line items</div>
        </div>
        <div style={{ textAlign: "right", minWidth: 96 }}><div style={{ fontWeight: 700, fontSize: 17, fontVariantNumeric: "tabular-nums" }}>{fmtINR(d.amount)}</div></div>
        <div className="row gap8">
          <button className="btn btn-sm" onClick={() => onOpen(d.id)}>Inspect</button>
          <button className="btn btn-danger btn-sm" onClick={() => setRejecting((v) => !v)}><Ic.x />Reject</button>
          <button className="btn btn-primary btn-sm" onClick={() => approve(d.id)}><Ic.check />Approve &amp; release</button>
        </div>
      </div>
      {rejecting && (
        <div className="card card-pad" style={{ marginTop: 12, background: "var(--bg-subtle, rgba(0,0,0,0.02))" }}>
          <div className="tiny" style={{ fontWeight: 600, marginBottom: 6 }}>Reason for rejection</div>
          <textarea className="input" style={{ width: "100%", minHeight: 72, resize: "vertical", marginBottom: 8 }}
                    placeholder="e.g. Wrong sales-return ledger — should post to Sales Return, not promotions."
                    value={reason} onChange={(e) => setReason(e.target.value)} disabled={working} />
          <div className="tiny muted" style={{ marginBottom: 8 }}>Your reason goes straight to Lekha AI — it reworks the draft, then you review the updated version before releasing to finance.</div>
          <div className="row gap8">
            <button className="btn btn-danger btn-sm" disabled={working || !reason.trim()}
                    onClick={async () => { setWorking(true); await reject(d.id, reason.trim()); setWorking(false); setRejecting(false); setReason(""); }}>
              {working ? "Lekha AI is reworking…" : <><Ic.x />Confirm reject</>}
            </button>
            <button className="btn btn-sm" onClick={() => { setRejecting(false); setReason(""); }} disabled={working}>Cancel</button>
          </div>
        </div>
      )}
    </div>
  );
}

function AdminDrafts({ docs, bizById, approve, reject, onOpen }) {
  if (!docs.length) return <EmptyState title="No drafts waiting" sub="Composed credit-note drafts appear here for Lekha review before the brand's finance team can see them." icon={<Ic.check />} />;
  return (
    <>
      <div className="page-head"><div><div className="ph-title">Draft Review</div><div className="ph-sub">Each draft was composed against the company's real Tally ledgers. Open to inspect the exact double entry, then approve to release it to brand finance.</div></div></div>
      <div className="stack gap12">
        {docs.map((d) => (
          <DraftRow key={d.id} d={d} biz={bizById(d.biz)} approve={approve} reject={reject} onOpen={onOpen} />
        ))}
      </div>
    </>
  );
}

/* ---- admin verify gate: SPLIT SCREEN — source PDF | editable OCR fields -- */
// PDF source: a faithful facsimile of the fetched debit note (placeholder
// sample CHC98139). When the real Step-1 fetch + per-doc PDF storage lands,
// point GATE_PDF at the document's own PDF URL.
const GATE_PDF = "/static/sample-debit-note.html";

// human-readable labels for the parser's consistency flags
const FLAG_LABEL = {
  "missing:note_no": "Debit note number missing",
  "missing:date": "Date missing",
  "missing:gstin": "GSTIN missing",
  "missing:invoice": "Invoice reference missing",
  "missing:total": "Total missing",
  "missing:lines": "No line items found",
  "subtotal_mismatch": "Line items don't sum to the printed subtotal",
  "total_mismatch": "Subtotal + tax ≠ the printed total",
};
function flagLabel(f) {
  if (FLAG_LABEL[f]) return FLAG_LABEL[f];
  const m = f.match(/^line:(\d+):(.+)$/);
  if (m) {
    const k = { missing_sku: "SKU missing", amount_mismatch: "Amount ≠ qty × rate" }[m[2]] || m[2];
    return `Line ${m[1]}: ${k}`;
  }
  return f;
}

function AdminGateDrawer({ doc, biz, store, onClose }) {
  const { adminEdit, adminApprove, adminReject } = store;
  const [invoice, setInvoice] = React.useState(doc.invoice);
  const [lines, setLines] = React.useState(() => doc.lines.map((l) => ({ ...l, item: l.tallyItem || "" })));
  const [detail, setDetail] = React.useState(null);
  const [skuOpts, setSkuOpts] = React.useState(null);

  React.useEffect(() => {
    api.get(`/api/credit-notes/${doc.id}`).then(setDetail).catch(() => {});
    api.get(`/api/credit-notes/${doc.id}/sku-options`).then(setSkuOpts).catch(() => {});
  }, [doc.id, doc.status]);

  // Pre-select the Tally item ONLY when the match is safe (already confirmed, or a
  // unique deterministic hit — incl. a unique PO MRP). Ambiguous / verbose lines are
  // left blank so the operator must choose from the candidate shortlist — never a guess.
  React.useEffect(() => {
    if (!skuOpts) return;
    setLines((ls) => ls.map((l) => {
      if (l.item) return l;
      const key = (l.desc || l.sku || "").trim();
      const s = (skuOpts.lines && skuOpts.lines[key]) || {};
      const safe = s.confidence === "confirmed" || s.confidence === "high";
      return { ...l, item: safe && s.stock_item ? s.stock_item : "" };
    }));
  }, [skuOpts]);

  React.useEffect(() => {
    const h = (e) => e.key === "Escape" && onClose();
    window.addEventListener("keydown", h);
    return () => window.removeEventListener("keydown", h);
  }, [onClose]);

  const setLine = (i, key, value) => {
    const strKey = key === "sku" || key === "desc" || key === "unit" || key === "item";
    setLines((ls) => ls.map((l, j) => (j === i ? { ...l, [key]: strKey ? value : Number(value) } : l)));
  };
  // Totals are document-level on these PDFs (no per-line GST) — show what was
  // printed/parsed, not a per-line recompute.
  const lineAmt = (l) => (l.amount != null ? l.amount : l.qty * l.rate);

  const save = () => adminEdit(doc.id, {
    fields: { invoice },
    lines: lines.map((l) => ({ sku: l.sku, desc: l.desc, unit: l.unit, qty: l.qty, rate: l.rate, gst: l.gst, amount: lineAmt(l), item: l.item })),
  });

  return (
    <div className="gate-modal" role="dialog" aria-modal="true">
      <div className="gm-head">
        <BizAvatar biz={biz} size={34} />
        <div style={{ minWidth: 0 }}>
          <div style={{ fontWeight: 700, fontSize: 16 }} className="mono">{doc.dnNo}</div>
          <div className="tiny">{biz.name} · verify the scanned fields against the source document</div>
        </div>
        <div className="spacer" />
        <Confidence value={doc.confidence} />
        <button className="icon-btn" onClick={onClose} style={{ marginLeft: 12 }}><Ic.x /></button>
      </div>

      <div className="gate-split">
        {/* LEFT — the fetched debit-note PDF, to verify against */}
        <div className="gp-doc">
          <iframe src={doc.pdfUrl ? api.mediaUrl(doc.pdfUrl) : GATE_PDF} title="Fetched debit note (source)" />
        </div>

        {/* RIGHT — the OCR-extracted fields, editable */}
        <div className="gp-fields">
          <div className="hint mb20"><Ic.shield style={{ marginTop: 1, flex: "0 0 15px" }} /><div>These fields were {doc.source === "ocr" ? "OCR-read from a scanned PDF" : "extracted from the PDF"} on the left. Check them, correct anything wrong, then approve to let Lekha generate the credit note.</div></div>

          {doc.flags && doc.flags.length > 0 && (
            <div className="card card-pad mb20" style={{ borderColor: "var(--amber-fg)", background: "var(--amber-bg)" }}>
              <div className="row gap8" style={{ marginBottom: 8 }}><Ic.shield style={{ flex: "0 0 15px", color: "var(--amber-fg)" }} /><div style={{ fontWeight: 700, fontSize: 13.5 }}>{doc.flags.length} thing{doc.flags.length > 1 ? "s" : ""} to verify against the source</div></div>
              {doc.flags.map((f, i) => <div className="tiny" key={i} style={{ padding: "2px 0" }}>• {flagLabel(f)}</div>)}
            </div>
          )}

          <div className="card card-pad mb20">
            <div className="row gap16" style={{ flexWrap: "wrap" }}>
              <div style={{ flex: "1 1 180px", minWidth: 0 }}>
                <div className="field-label">Invoice / reference number</div>
                <input className="input" style={{ width: "100%" }} value={invoice} onChange={(e) => setInvoice(e.target.value)} />
              </div>
            </div>
            <div className="tiny" style={{ marginTop: 12, lineHeight: 1.7 }}>
              <b>Vendor:</b> {doc.vendor || "—"} &nbsp;·&nbsp; <b>GSTIN:</b> <span className="mono">{doc.gstin || "—"}</span> &nbsp;·&nbsp; <b>Date:</b> {doc.docDate || "—"} &nbsp;·&nbsp; <b>Tax:</b> {doc.taxLabel || "—"}
            </div>
          </div>

          <div className="card card-pad">
            <div className="section-title">Line items — SKU mapping (editable)</div>
            <table className="li-tbl">
              <thead><tr><th>SKU</th><th>Description</th><th>Tally item</th><th className="num">Qty</th><th className="num">Rate</th><th className="num">Amount</th></tr></thead>
              <tbody>
                {lines.map((l, i) => (
                  <tr key={i}>
                    <td className="mono"><input className="input" style={{ width: 96, padding: "5px 8px" }} value={l.sku} onChange={(e) => setLine(i, "sku", e.target.value)} /></td>
                    <td>{l.desc}</td>
                    <td className="mono">
                      {skuOpts ? (() => {
                        const key = (l.desc || l.sku || "").trim();
                        const s = (skuOpts.lines && skuOpts.lines[key]) || {};
                        const cands = s.candidates || [];
                        const rest = (skuOpts.stock_items || []).filter((n) => !cands.includes(n));
                        return (
                          <select className="input" style={{ width: 210, padding: "5px 8px" }}
                            value={l.item || ""} onChange={(e) => setLine(i, "item", e.target.value)}>
                            <option value="">— pick Tally item —</option>
                            {cands.length > 0 && (
                              <optgroup label="Suggested">
                                {cands.map((n) => <option key={n} value={n}>{n}</option>)}
                              </optgroup>
                            )}
                            <optgroup label="All stock items">
                              {rest.map((n) => <option key={n} value={n}>{n}</option>)}
                            </optgroup>
                          </select>
                        );
                      })() : (l.item || l.tallyItem || <span style={{ color: "var(--text-3)" }}>…</span>)}
                    </td>
                    <td className="num"><input className="input" style={{ width: 56, padding: "5px 8px", textAlign: "right" }} value={l.qty} onChange={(e) => setLine(i, "qty", e.target.value)} /></td>
                    <td className="num"><input className="input" style={{ width: 78, padding: "5px 8px", textAlign: "right" }} value={l.rate} onChange={(e) => setLine(i, "rate", e.target.value)} /></td>
                    <td className="num">{fmtINR(lineAmt(l))}</td>
                  </tr>
                ))}
                {!lines.length && <tr><td colSpan="6"><div className="tiny" style={{ padding: "10px 0", color: "var(--text-3)" }}>No line items were found on the document.</div></td></tr>}
              </tbody>
              <tfoot>
                <tr><td colSpan="5">Subtotal</td><td className="num">{fmtINR(doc.taxable)}</td></tr>
                <tr><td colSpan="5">{doc.taxLabel || "Tax"}</td><td className="num">{fmtINR(doc.tax)}</td></tr>
                <tr><td colSpan="5" style={{ fontSize: 15 }}>Total</td><td className="num" style={{ fontSize: 15 }}>{fmtINR(doc.amount)}</td></tr>
              </tfoot>
            </table>
            <div className="tiny" style={{ marginTop: 8, color: "var(--text-3)" }}>Totals are taken from the document as printed (these debit notes carry tax at the document level).</div>
          </div>

          {/* Full append-only history — OCR, edits (who + old→new), gate decisions */}
          {detail && detail.history ? (
            <div className="mt20" style={{ marginTop: 20 }}><EventHistory events={detail.history} title="History — every change to this document" /></div>
          ) : null}
        </div>
      </div>

      <div className="gm-foot">
        <button className="btn btn-danger" onClick={() => { adminReject(doc.id); onClose(); }}><Ic.x />Reject</button>
        <button className="btn" onClick={save}>Save edits</button>
        <button className="btn btn-primary" onClick={() => { save(); adminApprove(doc.id); onClose(); }}><Ic.check />Approve</button>
      </div>
    </div>
  );
}

/* ---- all documents ------------------------------------------------------- */
function AdminAll({ docs, businesses, bizById, bizFilter, setBizFilter, statusFilter, setStatusFilter, onOpen }) {
  const [range, setRange] = React.useState("30d");
  let rows = docs.filter((d) => withinRange(d.createdAt, range));
  if (bizFilter !== "all") rows = rows.filter((d) => d.biz === bizFilter);
  if (statusFilter !== "all") rows = rows.filter((d) => d.status === statusFilter);
  return (
    <>
      <div className="page-head"><div><div className="ph-title">All Documents</div><div className="ph-sub">Master view across every business and pipeline stage.</div></div></div>
      <div className="toolbar">
        <select className="select" value={bizFilter} onChange={(e) => setBizFilter(e.target.value)}>
          <option value="all">All businesses</option>
          {businesses.map((b) => <option key={b.id} value={b.id}>{b.name}</option>)}
        </select>
        <select className="select" value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
          <option value="all">All statuses</option>
          {Object.keys(STATUS).map((k) => <option key={k} value={k}>{STATUS[k].label}</option>)}
        </select>
        <TimeFilter value={range} onChange={setRange} />
        <div className="spacer" />
      </div>
      <div className="card tbl-wrap">
        <table className="tbl">
          <thead><tr><th>Doc #</th><th>Business</th><th>Invoice</th><th>Amount</th><th>Status</th><th></th></tr></thead>
          <tbody>
            {rows.map((d) => {
              const biz = bizById(d.biz);
              const docNo = d.cnNo && d.cnNo !== "—" ? d.cnNo : d.dnNo;
              return (
                <tr key={d.id} onClick={() => onOpen(d.id)}>
                  <td className="doc-no">{docNo}</td>
                  <td><span className="row gap8"><BizAvatar biz={biz} size={22} />{biz.name}</span></td>
                  <td className="muted mono" style={{ fontSize: 13 }}>{d.invoice}</td>
                  <td className="amt">{fmtINR(d.amount)}</td>
                  <td><StatusBadge status={d.status} /></td>
                  <td style={{ width: 28 }}><Ic.chevron className="row-arrow" /></td>
                </tr>
              );
            })}
          </tbody>
        </table>
      </div>
    </>
  );
}

/* ---- accordion (FAQ-style expandable section) --------------------------- */
function Accordion({ icon, iconBg, iconFg, title, sub, defaultOpen, children }) {
  const [open, setOpen] = React.useState(!!defaultOpen);
  return (
    <div className={"acc" + (open ? " open" : "")}>
      <button className="acc-head" onClick={() => setOpen((o) => !o)} aria-expanded={open}>
        <span className="acc-ic" style={{ background: iconBg, color: iconFg }}>{icon}</span>
        <span style={{ minWidth: 0 }}>
          <span className="acc-tt" style={{ display: "block" }}>{title}</span>
          <span className="acc-sub" style={{ display: "block" }}>{sub}</span>
        </span>
        <Ic.chevron className="acc-chev" />
      </button>
      {open && <div className="acc-body">{children}</div>}
    </div>
  );
}

/* ---- settings — FAQ-style accordion: per-company gate + notifications ---- */
function AdminSettings({ businesses, setBizGate, setBizFinanceGate, pending, store }) {
  const gateOn = businesses.filter((b) => b.manual_approval).length;
  const finReq = businesses.filter((b) => b.finance_approval_required).length;
  return (
    <>
      <div className="page-head"><div><div className="ph-title">Settings</div><div className="ph-sub">Open a section to manage it. Verify gate is per company; notifications are master controls.</div></div></div>

      <Accordion
        icon={<Ic.shield />} iconBg="var(--amber-bg)" iconFg="var(--amber-fg)"
        title="Verify gate — per company"
        sub={`On for ${gateOn} of ${businesses.length} · controls which brands' debit notes you review before a credit note is generated`}
        defaultOpen
      >
        <div className="hint mb20" style={{ marginTop: 8 }}><Ic.shield style={{ marginTop: 1, flex: "0 0 15px" }} /><div>Turn the gate <b>on</b> for a new company so each fetched debit note waits in the Approval Queue for you to verify before Lekha generates the credit note. Switch it <b>off</b> once their debit notes are reliably clean — those flow straight through.{pending > 0 ? ` ${pending} waiting at the gate now.` : ""}</div></div>
        <div className="card">
          {businesses.map((b) => (
            <div className="biz-card" key={b.id}>
              <BizAvatar biz={b} size={40} />
              <div style={{ flex: "1 1 auto", minWidth: 0 }}>
                <div className="biz-name">{b.name}</div>
                <div className="biz-meta">{b.manual_approval ? "Gate on — debit notes wait for your review" : "Gate off — debit notes flow straight to credit-note creation"}</div>
              </div>
              <span className={"badge " + (b.manual_approval ? "amber" : "gray")} style={{ width: 84, justifyContent: "center" }}><span className="b-dot" />{b.manual_approval ? "On" : "Off"}</span>
              <label className="switch" style={{ flex: "0 0 46px" }}>
                <input type="checkbox" checked={b.manual_approval} onChange={(e) => setBizGate(b.id, e.target.checked)} />
                <span className="track"><span className="knob" /></span>
              </label>
            </div>
          ))}
        </div>
      </Accordion>

      <Accordion
        icon={<Ic.shield />} iconBg="var(--blue-bg)" iconFg="var(--blue-fg)"
        title="Finance approval — per company"
        sub={`Required for ${finReq} of ${businesses.length} · controls whether a brand's finance team must approve each credit note before it posts to Tally`}
      >
        <div className="hint mb20" style={{ marginTop: 8 }}><Ic.shield style={{ marginTop: 1, flex: "0 0 15px" }} /><div>Keep this <b>on</b> so each credit note waits for the brand's finance team to approve (by email or portal) before Lekha posts it to Tally. Switch it <b>off</b> to auto-approve credit notes the moment they're created and queue them straight for posting — the approval email becomes informational only. Posting itself is unchanged: an entry still only writes once the Lekha app is online and Tally is open.</div></div>
        <div className="card">
          {businesses.map((b) => (
            <div className="biz-card" key={b.id}>
              <BizAvatar biz={b} size={40} />
              <div style={{ flex: "1 1 auto", minWidth: 0 }}>
                <div className="biz-name">{b.name}</div>
                <div className="biz-meta">{b.finance_approval_required ? "Approval required — credit notes wait for finance" : "Approval off — credit notes auto-approve and queue for Tally"}</div>
              </div>
              <span className={"badge " + (b.finance_approval_required ? "amber" : "gray")} style={{ width: 84, justifyContent: "center" }}><span className="b-dot" />{b.finance_approval_required ? "Required" : "Off"}</span>
              <label className="switch" style={{ flex: "0 0 46px" }}>
                <input type="checkbox" checked={b.finance_approval_required} onChange={(e) => setBizFinanceGate(b.id, e.target.checked)} />
                <span className="track"><span className="knob" /></span>
              </label>
            </div>
          ))}
        </div>
      </Accordion>

      <Accordion
        icon={<Ic.mail />} iconBg="var(--blue-bg)" iconFg="var(--blue-fg)"
        title="Notifications — master control"
        sub="Decide which emails each persona is allowed to receive; they opt in or out within what you allow"
      >
        <AdminNotifications />
      </Accordion>

      <Accordion
        icon={<Ic.mail />} iconBg="var(--teal-bg)" iconFg="var(--teal-fg)"
        title="Recipient email IDs — per company"
        sub="The actual inboxes Lekha sends to for each business — finance (approval/review) and brand ops (status). Add as many as you like."
      >
        <AdminRecipients businesses={businesses} store={store} />
      </Accordion>
    </>
  );
}

/* ---- recipient email control (Lekha admin) ----------------------------- */
const RCHANNELS = [
  { key: "finance", label: "Finance — approval & review emails" },
  { key: "brand_ops", label: "Brand ops — generation & status updates" },
];

function AdminRecipients({ businesses, store }) {
  return (
    <>
      <div className="hint info mb20" style={{ marginTop: 8 }}>
        <Ic.mail style={{ marginTop: 1, flex: "0 0 15px" }} />
        <div>Add the email IDs Lekha communicates with for each company. Type an address and press <b>Enter</b> (or Add) to register it; add several per channel. These are the recipients used for live sends.</div>
      </div>
      <div className="stack gap12">
        {businesses.map((b) => <RecipientCard key={b.id} biz={b} store={store} />)}
        {!businesses.length && <div className="tiny" style={{ color: "var(--text-3)" }}>No businesses yet.</div>}
      </div>
    </>
  );
}

function RecipientCard({ biz, store }) {
  const [data, setData] = React.useState(null);
  React.useEffect(() => {
    let live = true;
    store.listRecipients(biz.id).then((d) => live && setData(d)).catch(() => live && setData({ finance: [], brand_ops: [] }));
    return () => { live = false; };
  }, [biz.id]);
  return (
    <div className="card card-pad">
      <div className="row gap10" style={{ marginBottom: 6 }}>
        <BizAvatar biz={biz} size={28} />
        <div className="biz-name">{biz.name}</div>
      </div>
      {RCHANNELS.map((c) => (
        <ChannelEditor key={c.key} label={c.label} channel={c.key} biz={biz} store={store}
          list={(data && data[c.key]) || []} onChange={setData} />
      ))}
    </div>
  );
}

function ChannelEditor({ label, channel, biz, store, list, onChange }) {
  const [val, setVal] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const add = async () => {
    const email = val.trim();
    if (!email || busy) return;
    setBusy(true);
    const out = await store.addRecipient(biz.id, channel, email);
    setBusy(false);
    if (out) { onChange(out); setVal(""); }
  };
  const remove = async (rid) => {
    const out = await store.removeRecipient(biz.id, rid);
    if (out) onChange(out);
  };
  return (
    <div style={{ marginTop: 14 }}>
      <div className="field-label">{label}</div>
      <div className="row gap8" style={{ flexWrap: "wrap", margin: "8px 0" }}>
        {list.map((r) => (
          <span key={r.id} className="badge gray" style={{ gap: 6, paddingRight: 6 }}>
            {r.email}
            <button className="icon-btn" style={{ width: 18, height: 18, padding: 0 }} title="Remove" onClick={() => remove(r.id)}><Ic.x /></button>
          </span>
        ))}
        {!list.length && <span className="tiny" style={{ color: "var(--text-3)" }}>No recipients yet — falls back to the company default.</span>}
      </div>
      <div className="row gap8">
        <input className="input" style={{ flex: 1 }} type="email" placeholder="name@company.com — press Enter to add"
          value={val} onChange={(e) => setVal(e.target.value)}
          onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); add(); } }} />
        <button className="btn btn-sm" disabled={busy || !val.trim()} onClick={add}>Add</button>
      </div>
    </div>
  );
}

/* ---- accounting exceptions (engine flagged — human-in-loop) ------------- */
/* ---- Feedback Loops (FB1–FB5) — live loop health from the tables ---------- */
function AdminFeedbackLoops({ store, onGoExceptions }) {
  const [data, setData] = React.useState(null);
  const [err, setErr] = React.useState(false);
  const load = React.useCallback(() => {
    setErr(false);
    store.loopsSummary().then(setData).catch(() => setErr(true));
  }, [store]);
  React.useEffect(() => { load(); }, [load]);

  if (err) return <EmptyState title="Couldn't load loop health" sub="Try refreshing." icon={<Ic.shield />} />;
  if (!data) return <div className="empty"><div className="e-s">Loading…</div></div>;

  const t = data.totals || {};
  const when = (iso) => { if (!iso) return "—"; try { return new Date(iso).toLocaleString(); } catch (e) { return iso; } };
  const Metric = ({ label, value, strong }) => (
    <div style={{ minWidth: 92 }}>
      <div style={{ fontWeight: 700, fontSize: strong ? 22 : 18, fontVariantNumeric: "tabular-nums", lineHeight: 1.1 }}>{value}</div>
      <div className="tiny muted" style={{ marginTop: 2 }}>{label}</div>
    </div>
  );
  return (
    <>
      <div className="page-head">
        <div>
          <div className="ph-title">Feedback Loops</div>
          <div className="ph-sub">The 5 feedback gates (FB1–FB5) that feed the Lekha AI agent — aggregated live from the tables (exceptions, learned rules, doc status). This is the source of truth, not a snapshot.</div>
        </div>
        <div className="spacer" />
        <button className="btn btn-sm" onClick={load}>Refresh</button>
      </div>

      <div className="card card-pad mb20">
        <div className="row" style={{ gap: 28, flexWrap: "wrap" }}>
          <Metric label="Open exceptions" value={t.openExceptions || 0} strong />
          <Metric label="Agent-resolved" value={t.agentResolved || 0} strong />
          <Metric label="Learned rules" value={t.learnedRules || 0} strong />
          <Metric label="Humans waiting" value={t.humanQueue || 0} strong />
          <Metric label="Resolved (total)" value={t.resolvedExceptions || 0} strong />
        </div>
      </div>

      <div className="section-title" style={{ marginTop: 4 }}>Loops by gate</div>
      <div className="stack gap12">
        {(data.gates || []).map((g) => {
          const active = (g.openExceptions + g.resolvedExceptions + g.learnedRules + g.humanQueue) > 0;
          return (
            <div key={g.gate} className="card card-pad" style={{ borderColor: g.openExceptions ? "var(--amber-fg)" : "var(--border)" }}>
              <div className="row gap10" style={{ justifyContent: "space-between", flexWrap: "wrap" }}>
                <span className="row gap8">
                  <span className="badge" style={{ background: "#1c1c1d", color: "#c5e063" }}>{g.gate}</span>
                  <span style={{ fontWeight: 600 }}>{g.label}</span>
                  <span className="tiny muted">reviewer: {g.reviewer}</span>
                </span>
                <span className="tiny muted">last activity: {when(g.lastActivity)}</span>
              </div>
              <div className="tiny muted" style={{ marginTop: 4 }}>Signal into the agent: {g.signal}</div>
              <div className="row" style={{ gap: 24, flexWrap: "wrap", marginTop: 12 }}>
                <Metric label="Open" value={g.openExceptions} />
                <Metric label="Resolved" value={g.resolvedExceptions} />
                <Metric label="Agent-resolved" value={g.agentResolved} />
                <Metric label="Learned rules" value={g.learnedRules} />
                <Metric label="Humans waiting" value={g.humanQueue} />
              </div>
              {g.gate === "FB2" && g.openExceptions > 0 && (
                <div style={{ marginTop: 10 }}>
                  <button className="btn btn-sm" onClick={onGoExceptions}>Open Exceptions →</button>
                </div>
              )}
              {!active && <div className="tiny muted" style={{ marginTop: 10 }}>No activity recorded on this loop yet.</div>}
            </div>
          );
        })}
      </div>

      {/* What the agent consumes as prompt context on a failed case. Grounded in
          ACO agent guidance: the agent's memory must be curated + human-readable
          (Anthropic, Writing effective tools) and learning is human-confirmed
          before load (OpenAI, agent = model + instructions + tools + guardrails). */}
      <div className="section-title" style={{ marginTop: 22 }}>Agent memory — what Lekha AI reads on a failed case</div>
      <div className="hint mb20"><Ic.shield style={{ marginTop: 1, flex: "0 0 15px" }} /><div>These human-confirmed rules are injected verbatim into <b>agent.cn-write</b>'s prompt (as “Learned rules for this company”) whenever it is handed feedback for a failed case. Nothing here was auto-learned — each was confirmed by a human before the agent could load it. This is the functional input the agent reasons from.</div></div>
      {!(data.agentMemory || []).length
        ? <EmptyState title="No learned rules yet" sub="When you resolve a case and save it as a rule, it appears here and the agent starts using it." icon={<Ic.check />} />
        : (
          <div style={{ overflowX: "auto" }}>
            <table className="tbl">
              <thead><tr><th>Gate</th><th>Scope</th><th>Action / note</th><th className="num">Hits</th><th>Confirmed by</th></tr></thead>
              <tbody>
                {data.agentMemory.map((r) => (
                  <tr key={r.id}>
                    <td><span className="badge" style={{ background: "#1c1c1d", color: "#c5e063" }}>{r.gate}</span></td>
                    <td className="tiny">{r.scope || "—"}</td>
                    <td className="tiny">{r.note || (r.action ? JSON.stringify(r.action) : "—")}</td>
                    <td className="num">{r.hits || 0}</td>
                    <td className="tiny muted">{r.confirmedBy || r.proposedBy || "—"}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}

      {/* The resolution ledger — every closed case and the input that closed it. */}
      <div className="section-title" style={{ marginTop: 22 }}>Resolution ledger — how each exception was resolved</div>
      <div className="hint mb20"><Ic.shield style={{ marginTop: 1, flex: "0 0 15px" }} /><div>Every resolved case: what failed, and the exact input that closed it — a deterministic option, a human's free-text note, or the agent's tool trace. This is the history the learned rules above are distilled from.</div></div>
      {!(data.resolutions || []).length
        ? <EmptyState title="No resolved cases yet" sub="Resolved exceptions and their fixes will be listed here." icon={<Ic.check />} />
        : (
          <div style={{ overflowX: "auto" }}>
            <table className="tbl">
              <thead><tr><th>Gate</th><th>Case</th><th>Failure</th><th>Resolved by</th><th>Input that resolved it</th><th>Rule?</th><th>When</th></tr></thead>
              <tbody>
                {data.resolutions.map((r) => (
                  <tr key={r.id}>
                    <td><span className="badge" style={{ background: "#1c1c1d", color: "#c5e063" }}>{r.gate}</span></td>
                    <td className="tiny">
                      <div><b>{r.cnNo || r.dnNo || ("#" + r.id)}</b></div>
                      {r.party && <div className="muted">{r.party}</div>}
                    </td>
                    <td className="tiny">{r.why || r.kind}</td>
                    <td className="tiny"><span className={"badge " + (r.by === "agent" ? "" : "amber")} style={r.by === "agent" ? { background: "#2a1c3a", color: "#c9a8ff" } : {}}>{r.by}</span></td>
                    <td className="tiny">
                      {r.note && <div>{r.note}</div>}
                      {!r.note && r.option && <div className="muted">option: {r.option}</div>}
                      {r.tools && r.tools.length > 0 && <div className="muted" style={{ marginTop: 2 }}>tools: {r.tools.join(" → ")}</div>}
                    </td>
                    <td className="tiny">{r.becameRule ? <span className="badge green">saved</span> : <span className="muted">—</span>}</td>
                    <td className="tiny muted">{when(r.at)}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
    </>
  );
}

function AdminExceptions({ store }) {
  const [items, setItems] = React.useState(null);
  const [filter, setFilter] = React.useState("all");   // all | OPEN | resolved
  const load = React.useCallback(() => {
    store.listExceptions(filter)
      .then((d) => setItems(d.items)).catch(() => setItems([]));
  }, [filter]);
  React.useEffect(() => { load(); }, [load]);

  if (!items) return <div className="empty"><div className="e-s">Loading…</div></div>;
  const isOpen = (e) => (e.status || "").toUpperCase() === "OPEN";
  // open first (need action), then resolved (audit record), each newest-first
  const sorted = [...items].sort((a, b) => (isOpen(a) ? 0 : 1) - (isOpen(b) ? 0 : 1));
  const FILTERS = [["all", "All"], ["OPEN", "Open"], ["resolved", "Resolved"]];
  return (
    <>
      <div className="page-head">
        <div><div className="ph-title">Exceptions</div><div className="ph-sub">Open items need a human; resolved ones STAY here as an audit record of who fixed what. No entry is ever faked.</div></div>
        <div className="spacer" />
        <div className="row gap4">
          {FILTERS.map(([f, label]) => (
            <button key={f} className={"btn btn-sm " + (filter === f ? "btn-primary" : "")} onClick={() => setFilter(f)}>{label}</button>
          ))}
        </div>
      </div>
      <div className="hint mb20"><Ic.shield style={{ marginTop: 1, flex: "0 0 15px" }} /><div>Each item is a document the engine could not compose or post cleanly (no reference invoice, unbalanced entry, missing ledger, unmapped SKU, Tally offline/rejection). <b>Open</b> ones hold in Processing — give feedback and the agent recomposes. <b>Resolved</b> ones remain for audit, tagged with who resolved them (finance / Lekha).</div></div>
      {!items.length && <EmptyState title={filter === "resolved" ? "No resolved exceptions yet" : "No exceptions"} sub={filter === "OPEN" ? "Nothing open — every flagged item has been resolved." : "Every composed credit note balanced and posted cleanly."} icon={<Ic.check />} />}
      <div className="stack gap12">
        {sorted.map((e) => <ExceptionCard key={e.id} exc={e} store={store} onResolved={load} />)}
      </div>
    </>
  );
}

function ExceptionCard({ exc, store, onResolved }) {
  const [note, setNote] = React.useState("");
  const [feedback, setFeedback] = React.useState("");
  const [busy, setBusy] = React.useState(false);
  const [busyAI, setBusyAI] = React.useState(false);
  const [aiResult, setAiResult] = React.useState(null);
  const open = (exc.status || "").toUpperCase() === "OPEN";
  const resolve = async (option) => {
    setBusy(true);
    const r = await store.resolveException(exc.id, option, note);
    setBusy(false);
    if (r) onResolved();
  };
  const giveFeedback = async () => {
    if (!feedback.trim()) return;
    setBusyAI(true);
    const r = await store.sendAgentFeedback(exc.id, feedback);
    setBusyAI(false);
    setAiResult(r);
    setFeedback("");
    // reload whether it fully resolved OR advanced to a new blocker, so the card
    // reflects the CURRENT state (a partial fix moves the exception to what's wrong now)
    if (r && (r.resolved || r.advanced_to)) onResolved();
  };
  const optLabel = (o) => (typeof o === "string" ? o : (o.label || o.id || "Resolve"));
  const optValue = (o) => (typeof o === "string" ? o : (o.id || o.label || "resolve"));
  const triedTools = aiResult && aiResult.trace ? aiResult.trace.map((t) => t.tool).filter(Boolean).join(" → ") : "";
  const c = exc.case || {};
  const money = (v) => (v == null ? null : (store.fmtINR ? store.fmtINR(v) : ("₹" + v)));
  // label → value; `ph` renders when the value is empty but its absence is signal
  // (missing invoice / nature are themselves the reason a case failed).
  const Fact = ({ label, value, ph }) => (
    (value != null && value !== "") || ph ? (
      <span className="tiny" style={{ marginRight: 16, whiteSpace: "nowrap" }}>
        <span className="muted">{label}:</span>{" "}
        <b>{(value != null && value !== "") ? value : ph}</b>
      </span>
    ) : null
  );
  const hasCase = exc.case && Object.keys(exc.case).length > 0;
  return (
    <div className="card card-pad" style={{ borderColor: open ? "var(--amber-fg)" : "var(--border)" }}>
      <div className="row gap10" style={{ justifyContent: "space-between", flexWrap: "wrap" }}>
        <span className="row gap8">
          <span className="badge amber"><span className="b-dot" />{exc.kind}</span>
          <span className="tiny">{exc.biz} · {exc.gateLabel || ("stage " + exc.stage)}{(exc.lekhaId || exc.docId) ? " · " + (exc.lekhaId || exc.docId) : ""}{exc.reasoner ? " · " + exc.reasoner : ""}</span>
        </span>
        <span className={"badge " + (open ? "amber" : "green")} style={{ justifyContent: "center", padding: "0 10px" }}><span className="b-dot" />{open ? "Open" : (exc.resolvedVia === "finance_review" ? "Resolved · by finance" : "Resolved · by Lekha")}</span>
      </div>
      {hasCase && (
        <div className="card" style={{ padding: "10px 12px", marginTop: 10, background: "var(--bg-subtle, rgba(0,0,0,0.02))" }}>
          <div className="tiny muted" style={{ fontWeight: 600, marginBottom: 6, textTransform: "uppercase", letterSpacing: ".04em" }}>Case</div>
          <div className="row" style={{ flexWrap: "wrap", rowGap: 4 }}>
            <Fact label="Debit note" value={c.dnNo} />
            <Fact label="Customer" value={c.party} />
            <Fact label="Credit note" value={c.cnNo || exc.lekhaId} />
            <Fact label="Original invoice" value={c.invoice} ph="— none —" />
            <Fact label="Nature" value={c.nature} ph="— not set —" />
          </div>
          <div className="row" style={{ flexWrap: "wrap", rowGap: 4, marginTop: 4 }}>
            <Fact label="Taxable" value={money(c.taxable)} />
            <Fact label="GST" value={money(c.tax)} />
            <Fact label="Total" value={money(c.total)} />
            <Fact label="GSTIN" value={c.gstin} />
          </div>
        </div>
      )}
      <div style={{ fontWeight: 600, marginTop: 10 }}>{exc.reason}</div>
      {exc.why && exc.why !== exc.reason && <div className="tiny" style={{ marginTop: 4, lineHeight: 1.6 }}><b>Why it failed:</b> {exc.why}</div>}
      {exc.explanation && <div className="tiny" style={{ marginTop: 8, lineHeight: 1.6 }}><b>Engine:</b> {exc.explanation}</div>}
      {exc.recommendation && <div className="tiny" style={{ marginTop: 4, lineHeight: 1.6 }}><b>Recommended:</b> {exc.recommendation}</div>}
      {open && (
        <div style={{ marginTop: 14 }}>
          {/* FB2 — give free-text guidance to the Lekha AI agent */}
          <div className="card" style={{ padding: 12, marginBottom: 12, background: "rgba(122,158,63,0.06)" }}>
            <div className="tiny" style={{ fontWeight: 600, marginBottom: 6 }}>Ask Lekha AI to fix it</div>
            <textarea className="input" rows={2} style={{ width: "100%", marginBottom: 8, resize: "vertical" }}
              placeholder={"Tell Lekha AI what to do — e.g. “book the offset to Sales Return - QC” or “post this as New Ref”."}
              value={feedback} onChange={(e) => setFeedback(e.target.value)} />
            <button className="btn btn-primary btn-sm" disabled={busyAI || !feedback.trim()} onClick={giveFeedback}>
              {busyAI ? "Lekha AI is working…" : "Give feedback to Lekha AI"}
            </button>
            {aiResult && (
              <div className="tiny" style={{ marginTop: 8, lineHeight: 1.5 }}>
                {aiResult.resolved
                  ? <span style={{ color: "var(--green-fg, #2e7d32)" }}><b>Lekha AI:</b> applied your feedback — the draft moved forward.{triedTools ? " (" + triedTools + ")" : ""}</span>
                  : aiResult.advanced_to
                  ? <span style={{ color: "var(--amber-fg, #b45309)" }}><b>Lekha AI:</b> applied your fix{triedTools ? " (" + triedTools + ")" : ""} — now blocked on the next issue: <b>{aiResult.advanced_to}</b>{aiResult.advanced_detail ? " — " + aiResult.advanced_detail : ""}. Give feedback for that below.</span>
                  : <span><b>Lekha AI:</b> {aiResult.provider === "disabled" ? "the agent is turned off in this environment."
                      : (/no key|unavailable/i.test(aiResult.provider || "") ? "not configured here (no API key)."
                      : ("couldn’t resolve it yet." + (triedTools ? " It tried: " + triedTools : " Try more specific guidance, or use a deterministic action below.")))}</span>}
              </div>
            )}
          </div>
          {/* Deterministic resolutions */}
          <input className="input" style={{ width: "100%", marginBottom: 8 }} placeholder="Note / original invoice number (used by ‘Supply the original invoice number’)" value={note} onChange={(e) => setNote(e.target.value)} />
          <div className="row gap8" style={{ flexWrap: "wrap", alignItems: "center" }}>
            <button className="btn btn-sm" disabled={busy}
              onClick={() => resolve(exc.recommendation || (exc.options && exc.options.length ? optValue(exc.options[0]) : "resolved"))}>
              {busy ? "Retrying…" : "Resolve & retry now"}
            </button>
            {(exc.options && exc.options.length > 0) && <span className="tiny muted">or:</span>}
            {(exc.options || []).map((o, i) => <button key={i} className="btn btn-sm" disabled={busy} onClick={() => resolve(optValue(o))}>{optLabel(o)}</button>)}
          </div>
          <div className="tiny muted" style={{ marginTop: 8, lineHeight: 1.5 }}>Give Lekha AI free-text guidance above, or use a deterministic action here (post as New Ref, supply the invoice, hold). Resolving re-runs the compose.</div>
        </div>
      )}
    </div>
  );
}

/* ---- Tally (desktop app)s (Step 6) — read-only monitor ---------------------- */
function relTime(iso) {
  if (!iso) return "never";
  const s = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 1000));
  if (s < 60) return s + "s ago";
  if (s < 3600) return Math.round(s / 60) + "m ago";
  return Math.round(s / 3600) + "h ago";
}
function AdminTally({ store }) {
  const { bridges } = store;
  const totalQueued = bridges.reduce((n, b) => n + (b.queued || 0), 0);
  return (
    <>
      <div className="page-head"><div><div className="ph-title">Tally Status</div><div className="ph-sub">Live connectivity of each finance team's Lekha desktop app — detected automatically.</div></div></div>
      <div className="hint info mb20"><Ic.tally style={{ marginTop: 1, flex: "0 0 15px" }} /><div>Connectivity is detected from each desktop app's heartbeat — there's no manual switch here. If the app is <b>offline</b>, that business's approved credit notes wait as <b>Approved (queued)</b>; when it comes back online they're written to Tally and move to <b>Confirmed</b>.{totalQueued > 0 ? ` ${totalQueued} credit note${totalQueued > 1 ? "s" : ""} queued right now.` : ""}</div></div>
      <div className="card">
        {bridges.map((b) => (
          <div className="biz-card" key={b.business_id}>
            <BizAvatar biz={{ short: b.short, color: b.color }} size={40} />
            <div style={{ flex: "1 1 auto", minWidth: 0 }}>
              <div className="biz-name">{b.name}</div>
              <div className="biz-meta">Last heartbeat: {relTime(b.last_seen)}</div>
            </div>
            {b.queued > 0 && (
              <span className="badge amber" style={{ flex: "0 0 auto" }}><span className="b-dot" />{b.queued} queued</span>
            )}
            <span className={"badge " + (b.connected ? "green" : "gray")} style={{ flex: "0 0 auto", width: 118, justifyContent: "center" }}>
              <span className="b-dot" />{b.connected ? "Connected" : "Disconnected"}
            </span>
          </div>
        ))}
        {!bridges.length && <div className="biz-card" style={{ justifyContent: "center", color: "var(--text-3)" }}>No devices connected.</div>}
      </div>
    </>
  );
}

/* ---- Tally Sync: replica freshness + binding console (PRD v2 step 8) ----- */
const ROLE_LABEL = {
  party: "Party (Sundry Debtors)", offset_returns: "Sales return", offset_damage: "Damaged / claims",
  offset_rate_diff: "Rate difference", tax_cgst: "CGST", tax_sgst: "SGST", tax_igst: "IGST",
  roundoff: "Round-off",
};
const STATE_BADGE = { BOUND: "green", ACTIVE: "green", BLOCKED: "amber", UNPROBED: "gray" };

function AdminReplica({ store }) {
  const { businesses } = store;
  const [bid, setBid] = React.useState(businesses[0] ? businesses[0].id : "");
  const [state, setState] = React.useState(null);
  const [busy, setBusy] = React.useState("");

  const load = (id) => { setState(null); api.get(`/api/tally/replica/${id}/state`).then(setState).catch(() => setState({ error: true })); };
  React.useEffect(() => { if (bid) load(bid); }, [bid]);

  const [diag, setDiag] = React.useState(null);
  const syncNow = (force) => {
    setBusy("sync"); setDiag(null);
    api.post(`/api/tally/replica/${bid}/sync${force ? "?force=true" : ""}`, {})
      .then((r) => { setDiag({ synced: r }); load(bid); })
      .catch((e) => setDiag({ error: String(e.message || e) }))
      .finally(() => setBusy(""));
  };
  const diagnose = () => {
    setBusy("diag"); setDiag(null);
    api.get(`/api/tally/replica/${bid}/diagnose`)
      .then(setDiag).catch((e) => setDiag({ error: String(e.message || e) }))
      .finally(() => setBusy(""));
  };
  const lastVoucher = () => {
    setBusy("lv"); setDiag(null);
    api.get(`/api/tally/replica/${bid}/last-voucher`)
      .then(setDiag).catch((e) => setDiag({ error: String(e.message || e) }))
      .finally(() => setBusy(""));
  };
  const reloadSamples = () => {
    setBusy("reload"); setDiag(null);
    api.post(`/api/tally/replica/${bid}/reload-samples`, {})
      .then((r) => setDiag({ reloaded: r })).catch((e) => setDiag({ error: String(e.message || e) }))
      .finally(() => setBusy(""));
  };
  const testPost = (qs) => {
    setBusy("tp"); setDiag(null);
    api.post(`/api/tally/replica/${bid}/test-post?date=20260620&${qs || ""}`, {})
      .then(setDiag).catch((e) => setDiag({ error: String(e.message || e) }))
      .finally(() => setBusy(""));
  };

  return (
    <>
      <div className="page-head"><div><div className="ph-title">Tally Sync</div><div className="ph-sub">The GUID-keyed replica of each client's Tally — masters &amp; vouchers, kept fresh by the background sync. Review the auto-derived ledger binding and confirm it here.</div></div></div>
      <div className="hint info mb20"><Ic.tally style={{ marginTop: 1, flex: "0 0 15px" }} /><div>Reading a client's Tally is <b>automatic</b> when their Lekha app is online — there's no client-facing button. This console shows what we've mirrored and lets ops confirm the role&rarr;ledger map (by stable GUID) before any credit note is posted.</div></div>

      <div className="row gap10 mb20" style={{ alignItems: "center" }}>
        <select className="select" value={bid} onChange={(e) => setBid(e.target.value)}>
          {businesses.map((b) => <option key={b.id} value={b.id}>{b.name}</option>)}
        </select>
        <button className="btn btn-sm" disabled={busy === "sync"} onClick={() => syncNow(false)}>{busy === "sync" ? "Syncing…" : "Sync now"}</button>
        <button className="btn btn-sm" disabled={busy === "sync"} onClick={() => syncNow(true)} title="Ignore watermarks and re-pull">Force full</button>
        <button className="btn btn-sm" disabled={busy === "diag"} onClick={diagnose} title="Live probe of the connected Tally — shows raw response">{busy === "diag" ? "Probing…" : "Diagnose Tally"}</button>
        <button className="btn btn-sm" disabled={busy === "lv"} onClick={lastVoucher} title="The last voucher we sent + Tally's response">{busy === "lv" ? "Loading…" : "Last Tally voucher"}</button>
        <button className="btn btn-sm" disabled={busy === "reload"} onClick={reloadSamples} title="Wipe this brand's docs (keeps binding) and load all samples as available">{busy === "reload" ? "Reloading…" : "Reload all sample docs"}</button>
        <button className="btn btn-sm" disabled={busy === "tp"} onClick={() => testPost("gstin=1")} title="₹1 test + PARTYGSTIN">{busy === "tp" ? "Posting…" : "Test (+gstin)"}</button>
        <button className="btn btn-sm" disabled={busy === "tp"} onClick={() => testPost("ref=1")} title="₹1 test + REFERENCE">Test (+ref)</button>
        <button className="btn btn-sm" disabled={busy === "tp"} onClick={() => testPost("real=1")} title="₹1 test mirroring the EXACT real voucher strings (slashes + narration)">Test (exact real)</button>
        <button className="btn btn-sm" disabled={busy === "tp"} onClick={() => testPost("real=1&amt=18672.00")} title="Real strings + the real ₹18,672 amount">Test (₹18,672)</button>
      </div>

      {diag && (
        <div className="card card-pad mb20" style={{ borderColor: "var(--amber-fg)" }}>
          <div className="section-title" style={{ marginTop: 0 }}>Tally diagnostic (raw)</div>
          <pre style={{ whiteSpace: "pre-wrap", wordBreak: "break-word", fontSize: 11.5, maxHeight: 460, overflow: "auto", margin: 0 }}>{JSON.stringify(diag, null, 2)}</pre>
        </div>
      )}

      {!state && <div className="empty"><div className="e-s">Loading…</div></div>}
      {state && state.error && <div className="empty"><div className="e-s">Could not load replica state.</div></div>}
      {state && !state.error && (
        <div className="stack gap16">
          <div className="card card-pad">
            <div className="row" style={{ justifyContent: "space-between", alignItems: "center" }}>
              <div className="section-title" style={{ margin: 0 }}>Replica</div>
              <span className={"badge " + (STATE_BADGE[state.provisioned_state] || "gray")} style={{ justifyContent: "center" }}><span className="b-dot" />{state.provisioned_state}</span>
            </div>
            <div className="biz-meta mt8">{state.tally_company_name || "(no company bound)"}{state.bound_company_guid ? " · " + state.bound_company_guid.slice(0, 8) : ""}</div>
            <div className="row gap16 mt12" style={{ flexWrap: "wrap" }}>
              <Stat k="Masters mirrored" v={state.masters} />
              <Stat k="Vouchers mirrored" v={state.vouchers} />
            </div>
            {/* REMASTER: sync state is per object_type (one company per tenant),
                not per company. Render each object_type's freshness row. */}
            {(!state.objects || !state.objects.length) && <div className="biz-meta mt12">No sync has run yet for this company.</div>}
            {(state.objects || []).map((o, i) => (
              <div className="biz-card" key={(o.object_type || "") + "|" + i}>
                <div style={{ flex: "1 1 auto", minWidth: 0 }}>
                  <div className="biz-name">{o.object_type}{o.replica_ready && <span className="badge green" style={{ marginLeft: 8 }}><span className="b-dot" />ready</span>}</div>
                  <div className="biz-meta">AlterID {o.last_alter_id} · Last poll {relTime(o.last_poll_at)} · Last delta {relTime(o.last_delta_at)}{o.reconcile_mismatch_count ? " · " + o.reconcile_mismatch_count + " mismatch" : ""}</div>
                </div>
                <span className={"badge " + (o.status === "IDLE" ? "green" : o.status === "ERROR" ? "amber" : "gray")} style={{ width: 96, justifyContent: "center" }}><span className="b-dot" />{o.status || "—"}</span>
              </div>
            ))}
          </div>
          <ReplicaBinding bid={bid} onConfirmed={() => load(bid)} />
        </div>
      )}
    </>
  );
}

function Stat({ k, v }) {
  return <div><div style={{ fontSize: 22, fontWeight: 700, fontVariantNumeric: "tabular-nums" }}>{v}</div><div className="biz-meta">{k}</div></div>;
}

const LEG_LABEL = {
  party: "Party (Sundry Debtors)", sales_return: "Sales return", cgst: "CGST",
  sgst: "SGST", igst: "IGST", cess: "Cess", round_off: "Round-off",
};

function EngineMapping({ bid, onConfirmed }) {
  const [ev, setEv] = React.useState(null);
  const [busy, setBusy] = React.useState(false);
  const reload = () => { setEv(null); api.get(`/api/tally/binding/${bid}/engine-view`).then(setEv).catch((e) => setEv({ error: String((e && e.message) || e || "failed") })); };
  React.useEffect(reload, [bid]);
  if (!ev) return <div className="card card-pad" style={{ marginBottom: 16 }}><div className="e-s">Loading engine binding…</div></div>;
  if (ev.error) return <div className="card card-pad" style={{ marginBottom: 16 }}><div className="hint amber"><div>Could not load the engine binding{typeof ev.error === "string" ? ": " + ev.error : ""}.</div></div></div>;
  // REMASTER: engine-view now returns { suggestion, confirmed_binding{lines[]} }.
  const sug = ev.suggestion || {};
  const cb = ev.confirmed_binding || {};
  const lines = cb.lines || [];
  const roles = sug.roles || {};
  const legLabel = (r, n) => (LEG_LABEL[r] || r) + (n ? " · " + n : "");
  const pill = (ok, txt) => <span className={"badge " + (ok ? "green" : "amber")} style={{ width: 110, justifyContent: "center" }}><span className="b-dot" />{txt || (ok ? "Bound" : "Missing")}</span>;
  const row = (name, meta, ok, k, txt) => (
    <div className="biz-card" key={k || name}>
      <div style={{ flex: "1 1 auto", minWidth: 0 }}><div className="biz-name">{name}</div><div className="biz-meta">{meta}</div></div>
      {pill(ok, txt)}
    </div>
  );
  const confirm = () => {
    setBusy(true);
    api.post(`/api/tally/binding/${bid}/confirm`, {})
      .then(() => { reload(); if (onConfirmed) onConfirmed(); })
      .finally(() => setBusy(false));
  };
  return (
    <div className="card card-pad" style={{ marginBottom: 16 }}>
      <div className="row" style={{ justifyContent: "space-between", alignItems: "center" }}>
        <div className="section-title" style={{ marginTop: 0 }}>Engine binding — derived from your Tally chart</div>
        <span className={"badge " + (cb.is_valid ? "green" : "amber")} style={{ justifyContent: "center" }}><span className="b-dot" />{cb.is_valid ? "Valid" : (cb.exists ? "Incomplete" : "Not confirmed")}</span>
      </div>
      <div className="biz-meta mb20">{sug.ledger_count || 0} ledgers · {(sug.party || {}).count || 0} debtor parties. Roles derive from each ledger's <b>group + GST metadata</b>; the party resolves per credit note by GSTIN. Voucher type: {cb.voucher_type_name || "—"}.</div>
      {cb.invalid_reason && <div className="hint amber mb20"><div>{cb.invalid_reason}</div></div>}
      <div className="biz-name" style={{ marginBottom: 6 }}>Confirmed binding ({lines.length})</div>
      {lines.length ? lines.map((l, i) => row(l.ledger, legLabel(l.leg_role, l.nature) + (l.gst_rate != null ? " · " + l.gst_rate + "%" : ""), true, "cb" + i, "Bound"))
        : <div className="biz-meta">No confirmed binding yet — confirm the derived one below.</div>}
      {Object.keys(roles).length > 0 && (
        <>
          <div className="biz-name" style={{ marginTop: 14, marginBottom: 6 }}>Derived suggestion</div>
          {Object.keys(roles).map((rk, i) => { const r = roles[rk] || {}; return row(r.name || "— none —", legLabel(r.leg_role, r.nature) + (r.confidence ? " · " + r.confidence : ""), !!r.guid, "sg" + i, r.guid ? "Found" : "Missing"); })}
        </>
      )}
      {(sug.missing_required || []).length > 0 && <div className="hint amber mt12"><div>Missing required roles: <b>{(sug.missing_required || []).join(", ")}</b> — posting blocks until these are bound.</div></div>}
      <div className="row mt12"><button className="btn btn-primary btn-sm" disabled={busy} onClick={confirm}>{busy ? "Confirming…" : (cb.exists ? "Re-confirm binding" : "Confirm binding")}</button></div>
    </div>
  );
}

// The engine mapping IS the binding view now — the old name-keyword "suggest &
// confirm" screen (deprecated Layer A) is gone; the engine derives the binding
// from the live replica, so there's nothing to hand-confirm.
function ReplicaBinding({ bid, onConfirmed }) {
  return <EngineMapping bid={bid} onConfirmed={onConfirmed} />;
}

/* ---- master notification matrix (Lekha admin) --------------------------- */
function AdminNotifications() {
  const [cells, setCells] = React.useState(null);
  React.useEffect(() => { api.get("/api/notifications/master").then((d) => setCells(d.cells)); }, []);
  const toggle = (c, allowed) =>
    api.put("/api/notifications/master", { role: c.role, notif_type: c.notif_type, allowed }).then((d) => setCells(d.cells));

  if (!cells) return <div className="empty"><div className="e-s">Loading…</div></div>;
  // group cells by email type for a tidy matrix
  const byType = {};
  cells.forEach((c) => { (byType[c.notif_type] = byType[c.notif_type] || { label: c.type_label, rows: [] }).rows.push(c); });

  return (
    <>
      <div className="hint info mb20" style={{ marginTop: 8 }}><Ic.mail style={{ marginTop: 1, flex: "0 0 15px" }} /><div>Turning a cell <b>off</b> here disables that email for everyone in that persona and <b>locks</b> their own toggle. An email sends only when it's allowed here <b>and</b> the user has opted in.</div></div>
      <div className="stack gap16">
        {Object.keys(byType).map((t) => (
          <div className="card" key={t}>
            <div className="card-pad" style={{ paddingBottom: 6 }}><div className="section-title" style={{ margin: 0 }}>{byType[t].label}</div></div>
            {byType[t].rows.map((c) => (
              <div className="biz-card" key={c.role + c.notif_type}>
                <div style={{ flex: "1 1 auto" }}>
                  <div className="biz-name">{c.role_label}</div>
                  <div className="biz-meta">{c.allowed ? "Allowed to receive this email" : "Blocked — persona cannot receive or enable this"}</div>
                </div>
                <span className={"badge " + (c.allowed ? "green" : "gray")} style={{ width: 84, justifyContent: "center" }}><span className="b-dot" />{c.allowed ? "Allowed" : "Off"}</span>
                <label className="switch" style={{ flex: "0 0 46px" }}>
                  <input type="checkbox" checked={c.allowed} onChange={(e) => toggle(c, e.target.checked)} />
                  <span className="track"><span className="knob" /></span>
                </label>
              </div>
            ))}
          </div>
        ))}
      </div>
    </>
  );
}

Object.assign(window, { AdminApp });
