/* ============================================================
   app.jsx — orchestration + live AI extraction pipeline
   ============================================================ */
/* useState/useEffect/useRef/useCallback come from helpers.jsx (shared global scope) */
const { useMemo } = React;

/* ---- robust JSON extraction from a model reply ---- */
function parseJSON(txt) {
  if (!txt) return null;
  let s = txt.indexOf("{"), e = txt.lastIndexOf("}");
  if (s < 0 || e < 0) return null;
  try { return JSON.parse(txt.slice(s, e + 1)); } catch (_) { return null; }
}

const EXTRACT_SCHEMA =
`Return ONLY compact JSON, no prose, no code fences. Schema:
{"vendor":str,"id":str,"lines":[{"svc":str,"detail":str,"invoiced":num,"reference":num,"delta":num,"verdict":"match"|"exception","clause":str,"why":str}],"totals":{"invoiced":num,"reference":num,"exception":num},"draftNote":str}
Rules: reference = the contracted/PO/rate-card amount for that line. delta = invoiced - reference (positive = overcharge; negative = credit owed). verdict "exception" when |delta|>=0.01 OR a contracted line is missing from the invoice. For matching lines set clause/why to "". For exceptions, clause = the exact contracted rule/rate cited, why = one plain sentence. totals.exception = sum of absolute deltas. draftNote: <110 words, addressed to the supplier, list each exception with its dollar impact and clause, ask for a corrected invoice or credit, and state nothing is paid until corrected. Never invent a match when a referenced document is absent.`;

async function callExtractor(rawText, refContext, scenarioId) {
  const prompt =
`You are an accounts-payable 3-way-match reconciliation engine. Extract every line from this OCR'd invoice and compare each to its contracted reference, flagging overcharges, quantity mismatches and missing credits.

INVOICE (ocr):
${rawText}

REFERENCE DATA (purchase order / goods-receipt / rate card):
${refContext || "No separate reference file supplied — use any contracted rates, caps or PO rates noted on the invoice itself as the reference."}

${EXTRACT_SCHEMA}`;
  // Canonical invoices pass their id → served canned (0 tokens); a pasted invoice omits it → live.
  const reply = await window.claude.complete(prompt, scenarioId);
  return parseJSON(reply);
}

/* reference context for the canonical queue invoices (steers the live match) */
const REF_CONTEXT = {
  "INV-1042":
`PO-7741 (carrier agr NL-FF-2024): linehaul Melbourne->Geelong $1750.00; pallet handling 16 @ $25.00; tailgate delivery $45.00; fuel levy CAPPED at 15.5% (Sch B 4.2); wait time/demurrage $50.00/hr, first 30 min free, billed to nearest 15 min (6.1). GRN-GEEL-3389: 16 pallets received; truck GPS on site 13:48-15:00 = 1h12.`,
  "INV-1048":
`PO-7744 / contract VE-NL-2025: network demand 142 kVA @ $14.20; energy 38,640 kWh @ $0.182; metering $49.50; SOLAR EXPORT CREDIT due at $0.071/kWh on exported energy (3). Meter NMI 6203 recorded 2,180 kWh exported in May = $154.78 credit owed.`,
};

/* build an invoice-shaped object from a live AI result (used for pasted invoices) */
function buildFromAI(ai, fallbackId) {
  const lines = (ai.lines || []).map((l, i) => {
    const verdict = l.verdict === "exception" ? "exception" : "match";
    return {
      id: "x" + i, svc: l.svc || "Line " + (i + 1), detail: l.detail || "",
      invoiced: +l.invoiced || 0, reference: +l.reference || 0, delta: +l.delta || 0,
      verdict, evidence: verdict === "exception" ? "x" + i : null,
      clause: l.clause || "", why: l.why || "",
    };
  });
  const t = ai.totals || {};
  const evidence = {};
  lines.filter((l) => l.verdict === "exception").forEach((l) => {
    const credit = l.delta < 0;
    evidence[l.id] = {
      title: l.svc + (credit ? " — credit owed" : " — variance found"),
      amount: window.RECON.fmtMoney(Math.abs(l.delta), !credit),
      summary: (l.why || "This line does not reconcile to the contracted reference.") +
        ` Invoiced <b>${window.RECON.fmtMoney(l.invoiced)}</b> vs contracted <b>${window.RECON.fmtMoney(l.reference)}</b>.`,
      cols: [
        { k: "Contracted rule", src: "extracted reference",
          clause: l.clause ? `<hit>${l.clause}</hit>` : "Contracted rate as supplied.",
          note: `Contracted amount: <b>${window.RECON.fmtMoney(l.reference)}</b>.` },
        { k: "As billed", src: fallbackId,
          kv: [["Billed", window.RECON.fmtMoney(l.invoiced)], ["Should be", window.RECON.fmtMoney(l.reference)], ["Variance", window.RECON.fmtMoney(l.delta, true)]],
          note: credit ? "Credit owed to you — not yet applied." : "Billed above the contracted rate." },
      ],
      reasoning: `Extracted this line from the invoice and compared it to the contracted reference. ${l.why || ""} Variance resolves to <span class='mono'>${window.RECON.fmtMoney(l.delta, true)}</span>. Surfaced for you to action — nothing is posted.`,
    };
  });
  const hasExc = lines.some((l) => l.verdict === "exception");
  return {
    id: ai.id || fallbackId, vendor: ai.vendor || "Pasted invoice", source: "scan",
    received: "just now", amount: +t.invoiced || 0, po: "—", grn: "—",
    status: "pending", wired: true, kind: hasExc ? "exception" : "clean", uploaded: true,
    raw: null, lines,
    totals: { invoiced: +t.invoiced || 0, reference: +t.reference || 0, exception: +t.exception || 0,
      exceptionType: lines.some((l) => l.delta < 0) && !lines.some((l) => l.delta > 0) ? "credit" : null },
    draft: ai.draftNote || "Draft unavailable.", evidence,
  };
}

function App() {
  const toast = useToast();
  const [invoices, setInvoices] = useState(window.RECON.invoices);
  const [selectedId, setSelectedId] = useState(null);
  const [processing, setProcessing] = useState(false);
  const [resolvedMap, setResolvedMap] = useState({});
  const [matchMeta, setMatchMeta] = useState(null); // {source, ms}
  const [ev, setEv] = useState({ open: false, data: null });
  const [upload, setUpload] = useState({ open: false, busy: false });

  const selected = useMemo(() => invoices.find((i) => i.id === selectedId) || null, [invoices, selectedId]);

  function openEvidence(key) {
    if (!selected || !selected.evidence) return;
    setEv({ open: true, data: selected.evidence[key] });
  }

  // select an invoice → shimmer → reveal; fire a genuine live match in parallel
  function selectInvoice(id) {
    setEv({ open: false, data: null });
    setSelectedId(id);
    const inv = invoices.find((i) => i.id === id);
    if (!inv) return;
    // already resolved or already-verified pre-processed → no shimmer
    if (resolvedMap[id] || (inv.status !== "pending" && !inv.wired) || (inv.status === "verified")) {
      setProcessing(false); setMatchMeta(inv.wired ? { source: "cached" } : null); return;
    }
    if (!inv.wired) { setProcessing(false); setMatchMeta(null); return; }

    setProcessing(true);
    setMatchMeta(null);
    const t0 = performance.now();
    // genuine live call (confirms the canonical match; non-blocking)
    if (inv.raw) {
      callExtractor(inv.raw, REF_CONTEXT[inv.id], inv.id).then((ai) => {
        const ms = Math.round(performance.now() - t0);
        if (ai && ai.totals) {
          const within = Math.abs((+ai.totals.exception || 0) - inv.totals.exception) < 0.02;
          setMatchMeta({ source: within ? "live" : "live-approx", ms });
        } else { setMatchMeta({ source: "cached", ms }); }
      }).catch(() => setMatchMeta({ source: "cached" }));
    }
    setTimeout(() => setProcessing(false), 1550);
  }

  function resolve(kind) {
    if (!selected) return;
    const statusMap = { cleared: selected.kind === "clean" ? "verified" : "cleared", disputed: "disputed", review: "review" };
    const newStatus = statusMap[kind];
    setResolvedMap((m) => ({ ...m, [selected.id]: kind }));
    setInvoices((list) => list.map((i) => i.id === selected.id ? { ...i, status: newStatus } : i));
    const meta = {
      cleared: { title: selected.kind === "clean" ? "Cleared for payment" : "Cleared — accepted as billed", sub: "Routed to AP · marked verified", icon: "check", tone: "cleared" },
      disputed: { title: "Dispute sent to vendor", sub: `${selected.vendor} · ${window.RECON.fmtMoney(selected.totals ? selected.totals.exception : 0)} queried`, icon: "send", tone: "disputed" },
      review: { title: "Flagged for review", sub: "Escalated for a manual check", icon: "flag", tone: "flagged" },
    }[kind];
    toast(meta);
  }

  async function runUpload(text) {
    setUpload((u) => ({ ...u, busy: true }));
    try {
      const ai = await callExtractor(text, null);
      if (!ai || !ai.lines || !ai.lines.length) {
        toast({ title: "Couldn't read that invoice", sub: "Try the sample, or paste clearer line items.", icon: "alertTri", tone: "flagged" });
        setUpload((u) => ({ ...u, busy: false })); return;
      }
      const newInv = buildFromAI(ai, "PASTE-" + (Math.floor(Math.random() * 9000) + 1000));
      setInvoices((list) => [newInv, ...list]);
      setUpload({ open: false, busy: false });
      setSelectedId(newInv.id);
      setProcessing(true);
      setMatchMeta({ source: "live", ms: null });
      setTimeout(() => setProcessing(false), 1400);
      toast({ title: "Invoice extracted", sub: `${newInv.lines.length} lines · ${newInv.totals.exception ? window.RECON.fmtMoney(newInv.totals.exception) + " in exceptions" : "clean match"}`, icon: "zap", tone: "ok" });
    } catch (e) {
      toast({ title: "Extraction failed", sub: "The live model couldn't be reached. Try again.", icon: "alertTri", tone: "flagged" });
      setUpload((u) => ({ ...u, busy: false }));
    }
  }

  const resolvedKind = selected ? resolvedMap[selected.id] : null;
  const showResolved = !!resolvedKind;

  return (
    <div className="app">
      <header className="hdr">
        <Wordmark />
        <div className="sep"></div>
        <div className="appname">
          <span className="t">Invoice-to-PO Reconciler</span>
          <span className="s">3-way match · exceptions engine</span>
        </div>
        <div className="spacer"></div>
        <div className="status"><span className="dot"></span>live · model responding · p50 {matchMeta && matchMeta.ms ? matchMeta.ms + "ms" : "460ms"}</div>
        <div className="noledger"><Icon name="lock" size={13} />never posts to the ledger</div>
      </header>

      <div className="body3">
        <Queue invoices={invoices} selectedId={selectedId} onSelect={selectInvoice} onUpload={() => setUpload({ open: true, busy: false })} />
        <Analysis inv={selected} processing={processing} onEvidence={openEvidence} matchMeta={matchMeta} />
        <Resolution inv={selected} processing={processing} resolved={showResolved} resolveKind={resolvedKind} onResolve={resolve} onEvidence={openEvidence} />
      </div>

      <Evidence data={ev.data} open={ev.open} onClose={() => setEv({ open: false, data: null })} />
      <UploadModal open={upload.open} busy={upload.busy} onClose={() => setUpload({ open: false, busy: false })} onRun={runUpload} />
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<ToastProvider><App /></ToastProvider>);
