// financial/src/documents.jsx — the Documents tab: the drop-zone portal.
//
// Drop any financial document. It is uploaded to Cloud Storage (kept — a
// later, better extractor can re-read it), a record is written, a cheap
// classify call names its KIND, and the kind routes it: payslips and tax
// returns go to the parsers that already exist on their tabs, statements
// and policies are extracted here (lib/documentKinds shapes, nulls over
// guesses, a source for every figure). Every extraction is shown next to
// its warnings and applied only when a human clicks:
//
//   401(k) statement  → the money-source SPLIT into retirementConfig
//                       (the balance stays live from Monarch); the newest-
//                       DATED statement is the one in force, never the
//                       last uploaded
//   any account       → its fund positions (401(k)/403(b)/401(a), UK
//   statement           pension, 529, brokerage) become holdings for an
//                       account Monarch does not itemize — no click, since
//                       they are data rather than a config choice
//                       (lib/statementPositions → derived/statementHoldings)
//   tax return        → the Tax tab's own save, after review
//   insurance policy  → a row on the Estate tab's policies, after review
//   everything else   → history only
//
// The cadence panel says which kinds are overdue; the same audit feeds the
// plan-completeness card and the insight rule, so the prompt appears on the
// pages already being opened.

const { useState, useEffect, useContext, useMemo, useRef, useCallback } = React;
const { DataContext, useToast, callFn, fmtCur, fmtDate, timeAgo } = window.FinanceShared;

// The upload itself is the DataProvider's uploadDocumentFile — the ONE path
// every surface (this drop zone, the payslip buttons) puts a file through.
// The contract behind it (limits, path, error table, pending/reconcile) is
// lib/documentUpload, mirrored and tested. Read defensively: the HTML and
// the slices are cached independently, and a slice must stay harmless
// against HTML that has not loaded the lib yet.
const DU = window.DocumentUpload || null;
const MAX_FILE_MB = DU ? Math.round(DU.MAX_FILE_BYTES / 1024 / 1024) : 7;
const readBase64 = (file) => new Promise((resolve, reject) => {
  const r = new FileReader();
  r.onload = () => resolve(String(r.result).split(',')[1]);
  r.onerror = reject;
  r.readAsDataURL(file);
});
const KIND_COLOR = { payslip: '#3b82f6', taxReturn: '#f59e0b', k401Statement: '#8b5cf6', ukPensionStatement: '#6366f1', brokerageStatement: '#06b6d4', plan529Statement: '#64748b', insurancePolicy: '#10b981', other: '#475569' };

function Tag({ color, children }) {
  return <span style={{ fontSize: 9, color, border: `1px solid ${color}55`, borderRadius: 4, padding: '1px 5px', letterSpacing: '0.04em', whiteSpace: 'nowrap' }}>{children}</span>;
}

// ── One document's review panel ────────────────────────────────────────────
function money(v) { return v == null ? '—' : fmtCur(v); }
function ExtractionView({ doc }) {
  const x = doc.extracted;
  if (!x) return null;
  const rows = [];
  const push = (k, v) => { if (v !== undefined && v !== null && typeof v !== 'object') rows.push([k, typeof v === 'number' ? (Math.abs(v) >= 100 ? fmtCur(v) : String(v)) : String(v)]); };
  Object.entries(x).forEach(([k, v]) => { if (k !== 'sources' && k !== 'notes' && k !== 'moneySources' && k !== 'holdings' && k !== 'beneficiaries' && typeof v !== 'object') push(k, v); });
  ['contributionsThisPeriod', 'contributionsYtd'].forEach((g) => { if (x[g] && typeof x[g] === 'object') Object.entries(x[g]).forEach(([k, v]) => push(`${g}.${k}`, v)); });
  return (
    <div style={{ fontSize: 11 }}>
      {x.moneySources && x.moneySources.length > 0 && (
        <table style={{ fontSize: 11, marginBottom: 8 }}>
          <thead><tr><th>Money source</th><th style={{ textAlign: 'right' }}>Balance</th><th style={{ textAlign: 'right' }}>Vested</th><th>Tax</th></tr></thead>
          <tbody>{x.moneySources.map((r, i) => (
            <tr key={i}><td style={{ padding: '3px 8px' }}>{r.source}</td><td style={{ padding: '3px 8px', textAlign: 'right' }}>{money(r.balance)}</td><td style={{ padding: '3px 8px', textAlign: 'right' }}>{r.vestedPct == null ? '—' : `${r.vestedPct}%`}</td><td style={{ padding: '3px 8px', color: r.tax === 'roth' ? '#10b981' : '#8b5cf6' }}>{r.tax}</td></tr>
          ))}</tbody>
        </table>
      )}
      {Array.isArray(x.holdings) && x.holdings.length > 0 && (
        <table style={{ fontSize: 11, marginBottom: 8 }}>
          <thead><tr><th>Fund</th><th>Ticker / CUSIP</th><th style={{ textAlign: 'right' }}>Units</th><th style={{ textAlign: 'right' }}>Unit price</th><th style={{ textAlign: 'right' }}>Value</th><th style={{ textAlign: 'right' }}>% of balance</th></tr></thead>
          <tbody>{x.holdings.map((h, i) => (
            <tr key={i}><td style={{ padding: '3px 8px' }}>{h.fund}</td><td style={{ padding: '3px 8px', color: '#94a3b8' }}>{h.ticker || h.cusip || '—'}</td>
              <td style={{ padding: '3px 8px', textAlign: 'right' }}>{h.units == null ? '—' : h.units.toLocaleString('en-US', { maximumFractionDigits: 4 })}</td>
              <td style={{ padding: '3px 8px', textAlign: 'right' }}>{h.unitPrice == null ? '—' : `$${h.unitPrice.toLocaleString('en-US', { maximumFractionDigits: 4 })}`}</td>
              <td style={{ padding: '3px 8px', textAlign: 'right' }}>{money(h.marketValue)}</td>
              <td style={{ padding: '3px 8px', textAlign: 'right' }}>{h.pctOfBalance == null ? '—' : `${h.pctOfBalance}%`}</td></tr>
          ))}</tbody>
        </table>
      )}
      {Array.isArray(x.holdings) && x.holdings.length === 0 && <div style={{ fontSize: 10, color: '#64748b', marginBottom: 6 }}>No fund positions on this statement.</div>}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(220px,1fr))', gap: '2px 12px' }}>
        {rows.map(([k, v]) => <div key={k} style={{ display: 'flex', justifyContent: 'space-between', gap: 8, color: '#94a3b8' }}><span title={x.sources && x.sources[k] ? `source: ${x.sources[k]}` : ''}>{k}</span><span style={{ color: '#e2e8f0' }}>{v}</span></div>)}
      </div>
      {x.beneficiaries && x.beneficiaries.length > 0 && <div style={{ color: '#94a3b8', marginTop: 4 }}>beneficiaries: {x.beneficiaries.join(', ')}</div>}
      {x.notes && <div style={{ color: '#f59e0b', marginTop: 4 }}>note: {x.notes}</div>}
      {x.sources && <div style={{ fontSize: 9, color: '#475569', marginTop: 4 }}>Hover a field for the page and line it was read from.</div>}
    </div>
  );
}

function DocumentRow({ doc, onKind, onExtract, onApply, onDelete, onOpen, busy, applied, accounts, onLink }) {
  const DK = window.DocumentKinds;
  const [open, setOpen] = useState(doc.status === 'extracted' && !doc.appliedAt);
  const [armed, setArmed] = useState(false);
  const k = DK.kindOf(doc.kind) || DK.kindOf('other');
  const canExtract = k.route === 'extract';
  const canApply = doc.status === 'extracted' && doc.extracted && (
    (doc.kind === 'k401Statement' && doc.extracted.moneySources && doc.extracted.moneySources.length) ||
    (doc.kind === 'insurancePolicy' && doc.extracted.deathBenefit != null) ||
    (doc.kind === 'taxReturn' && doc.extracted.taxYear)
  );
  const statusColor = { pending: '#f59e0b', uploaded: '#64748b', classified: '#3b82f6', extracted: '#f59e0b', applied: '#10b981', error: '#ef4444', routed: '#10b981' }[doc.status] || '#64748b';
  return (
    <div style={{ borderTop: '1px solid #161b22', padding: '8px 0' }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
        <Tag color={KIND_COLOR[doc.kind] || '#475569'}>{k.label}</Tag>
        <select value={doc.kind || 'other'} onChange={e => onKind(doc, e.target.value)} style={{ width: 'auto', fontSize: 11, padding: '2px 6px' }} disabled={busy}>
          {DK.KINDS.map(x => <option key={x.key} value={x.key}>{x.label}</option>)}
        </select>
        <span style={{ fontSize: 12, color: '#e2e8f0', flex: 1, minWidth: 160 }}>{doc.fileName}</span>
        <span style={{ fontSize: 11, color: '#94a3b8' }}>{doc.statementDate ? `dated ${doc.statementDate}` : 'no statement date'}{doc.institution ? ` · ${doc.institution}` : ''}{doc.who ? ` · ${doc.who}` : ''}</span>
        <span style={{ fontSize: 10, color: statusColor }}>{doc.status}{doc.appliedAt ? ` ${fmtDate(doc.appliedAt)}` : ''}</span>
        {onLink && doc.extracted && window.DocumentReconcile && (() => {
          const cands = window.DocumentReconcile.linkCandidates(doc.kind, accounts);
          if (!cands.length) return null;
          return (
            <select value={doc.accountId || ''} onChange={e => onLink([doc.id], e.target.value || null)} disabled={busy} title="The account this statement is of — it is compared to that account only"
              style={{ width: 'auto', maxWidth: 180, fontSize: 10, padding: '2px 4px', color: doc.accountId ? '#e2e8f0' : '#64748b' }}>
              <option value="">account: auto</option>
              {cands.map(a => <option key={a.id} value={a.id}>{a.displayName || a.name || a.id}</option>)}
            </select>
          );
        })()}
        {doc.classification && <span style={{ fontSize: 9, color: '#475569' }}>classified {Math.round((doc.classification.confidence || 0) * 100)}%</span>}
        <span style={{ display: 'flex', gap: 4 }}>
          {doc.downloadUrl && <button className="btn-secondary" style={{ fontSize: 10, padding: '2px 8px' }} onClick={() => onOpen(doc)}>PDF</button>}
          {canExtract && <button className="btn-secondary" style={{ fontSize: 10, padding: '2px 8px' }} disabled={busy} onClick={() => onExtract(doc)}>{doc.extracted ? 'Re-extract' : 'Extract'}</button>}
          {doc.extracted && <button className="btn-secondary" style={{ fontSize: 10, padding: '2px 8px' }} onClick={() => setOpen(v => !v)}>{open ? 'Hide' : 'Review'}</button>}
          {canApply && !doc.appliedAt && <button className="btn-primary" style={{ fontSize: 10, padding: '2px 8px' }} disabled={busy} onClick={() => onApply(doc)}>{applied[doc.kind] || 'Apply'}</button>}
          <button className={armed ? 'btn-danger' : 'btn-secondary'} style={{ fontSize: 10, padding: '2px 8px' }} onClick={() => { if (armed) onDelete(doc); else setArmed(true); }}>{armed ? 'Confirm delete' : '✕'}</button>
        </span>
      </div>
      {doc.status === 'pending' && <div style={{ fontSize: 10, color: '#f59e0b', marginTop: 4 }}>Upload not finished — the record is settled on the next load once it is an hour old (promoted if the file landed, removed if not).</div>}
      {doc.error && <div style={{ fontSize: 11, color: '#ef4444', marginTop: 4 }}>{doc.error}</div>}
      {doc.warnings && doc.warnings.length > 0 && <div style={{ fontSize: 10, color: '#f59e0b', marginTop: 4 }}>{doc.warnings.map((w, i) => <div key={i}>⚠ {w}</div>)}</div>}
      {open && doc.extracted && <div style={{ marginTop: 8, padding: '8px 10px', background: '#0b1220', borderRadius: 8 }}><ExtractionView doc={doc} /></div>}
    </div>
  );
}

// ── The tab ────────────────────────────────────────────────────────────────
function Documents({ setTab }) {
  const { uid, documents, documentUsage, saveDocument, deleteDocument, uploadDocumentFile, saveRetirementConfig, saveTaxReturn, updateEstatePolicies, estatePlan, kidsPlan, retirementConfig, monarchAuth, reload, accounts, netWorthSnapshots, accountBalanceHistory, monarchHoldings, statementHoldings } = useContext(DataContext);
  const { show, Toast } = useToast();
  const DK = window.DocumentKinds;
  const [dragOver, setDragOver] = useState(false);
  const [busyIds, setBusyIds] = useState({});
  const [queue, setQueue] = useState([]);   // { name, status, error }
  const fileRef = useRef(null);
  const setBusy = (id, v) => setBusyIds(b => ({ ...b, [id]: v }));

  const cadence = useMemo(() => DK ? DK.cadenceAudit(documents || []) : null, [DK, documents]);
  // Which statement each applied field came from, and whether a newer-DATED
  // one is waiting (lib/documentKinds.appliedFieldStatus).
  const appliedFields = useMemo(() => DK && DK.appliedFieldStatus ? DK.appliedFieldStatus(retirementConfig, documents || []) : [], [DK, retirementConfig, documents]);
  // The latest statement of each account extracted before fund positions
  // were read — one re-extract each turns it into positions.
  const SP = window.StatementPositions;
  const positionRereads = useMemo(() => SP ? SP.rereadCandidates({ documents: documents || [], accounts, monarchHoldings: monarchHoldings || [], accountMap: retirementConfig && retirementConfig.accountMap }) : [], [SP, documents, accounts, monarchHoldings, retirementConfig]);
  const statementCoverage = useMemo(() => statementHoldings && statementHoldings.accounts ? Object.values(statementHoldings.accounts) : [], [statementHoldings]);
  const month = DK ? DK.monthKey() : '';
  // The month's cap: the standing MONTHLY_CALL_CAP plus any dated one-time
  // allowance (a bulk backfill month) — one number, from the lib.
  const monthCap = DK ? (DK.capForMonth ? DK.capForMonth(month) : DK.MONTHLY_CALL_CAP) : '—';
  // `months` counts extractions (the capped budget) only from usage schema 2
  // on; before that it was classifications and extractions mixed, and the
  // callable retires it to `legacyMonths` on the next drop.
  const usageCurrent = documentUsage && DK && documentUsage.schema === DK.USAGE_SCHEMA;
  const used = usageCurrent && documentUsage.months ? Number(documentUsage.months[month] || 0) : 0;
  const classified = usageCurrent && documentUsage.classifyMonths ? Number(documentUsage.classifyMonths[month] || 0) : 0;

  // ── The pipeline for one file ──────────────────────────────────────────
  const classifyAndRoute = useCallback(async (doc, base64, mimeType) => {
    setBusy(doc.id, true);
    try {
      const res = await callFn('processDocument', { action: 'classify', fileBase64: base64, mimeType, fileName: doc.fileName });
      if (res.paused) { await saveDocument({ id: doc.id, status: 'uploaded', error: res.reason }); show(res.reason, 'error'); return; }
      const c = res.classification;
      const next = { id: doc.id, status: 'classified', kind: c.kind, classification: c, statementDate: c.statementDate, institution: c.institution, who: c.who, accountHint: c.accountHint, error: null };
      await saveDocument(next);
      const k = DK.kindOf(c.kind);
      if (k.route === 'extract') await extract({ ...doc, ...next }, base64, mimeType);
      else if (k.route === 'parsePayslip') {
        // The existing parser saves the payslip itself under compensation/{year}/payslips.
        const r = await callFn('parsePayslip', { imageBase64: base64, mimeType, fallbackYear: c.statementDate ? Number(c.statementDate.slice(0, 4)) : new Date().getFullYear() });
        await saveDocument({ id: doc.id, status: 'routed', routedTo: 'parsePayslip', extracted: r && r.parsed ? r.parsed : r || null, appliedAt: new Date().toISOString() });
        reload();
      } else if (k.route === 'parseTaxReturn') {
        const r = await callFn('parseTaxReturn', { fileBase64: base64, mimeType, fallbackYear: c.statementDate ? Number(c.statementDate.slice(0, 4)) - 1 : new Date().getFullYear() - 1 });
        await saveDocument({ id: doc.id, status: 'extracted', routedTo: 'parseTaxReturn', extracted: { taxYear: r.year, ...r.parsed }, warnings: r.warnings || [], statementDate: doc.statementDate || `${r.year}-12-31` });
      }
    } catch (e) {
      await saveDocument({ id: doc.id, status: 'error', error: e.message });
      show(e.message, 'error');
    }
    setBusy(doc.id, false);
  }, [saveDocument, show, reload]);

  // Returns { ok } or { ok:false, exhausted } — `exhausted` is the callable's
  // resource-exhausted (the month's cap genuinely reached), which the bulk
  // retry below treats as "stop", not "next".
  const budgetExhausted = (e) => /resource-exhausted/.test(String((e && e.code) || '')) || /budget for .* is used up/i.test(String((e && e.message) || ''));
  const extract = useCallback(async (doc, base64, mimeType, opts) => {
    const quiet = !!(opts && opts.quiet);
    setBusy(doc.id, true);
    let out = { ok: false };
    try {
      // A re-read names the kept file by its object path and the callable
      // reads it server-side: fetching the download URL back from here is a
      // cross-origin request the bucket does not grant ("Failed to fetch").
      const req = { action: 'extract', kind: doc.kind, fileName: doc.fileName, mimeType: mimeType || doc.mimeType };
      if (base64) req.fileBase64 = base64;
      else {
        const sp = DU ? DU.storagePathOf(doc) : doc.storagePath;
        if (!sp) throw new Error('This record has no kept file to re-read — upload the statement again.');
        req.storagePath = sp;
      }
      const res = await callFn('processDocument', req);
      if (res.paused) { await saveDocument({ id: doc.id, error: res.reason }); if (!quiet) show(res.reason, 'error'); out = { ok: false, paused: true }; }
      else {
        const x = res.extracted || {};
        await saveDocument({ id: doc.id, status: 'extracted', extracted: x, warnings: res.warnings || [], error: null,
          statementDate: x.statementDate || x.effectiveDate || doc.statementDate || null, institution: x.provider || x.institution || x.insurer || doc.institution || null, extractedAt: new Date().toISOString() });
        if (!quiet) show(`Read the ${DK.kindOf(doc.kind).label.toLowerCase()} — review before applying`);
        out = { ok: true };
      }
    } catch (e) {
      await saveDocument({ id: doc.id, status: 'error', error: e.message });
      if (!quiet) show(e.message, 'error');
      out = { ok: false, exhausted: budgetExhausted(e), message: e.message };
    }
    setBusy(doc.id, false);
    return out;
  }, [saveDocument, show]);

  // ── Bulk retry of failed extractions ───────────────────────────────────
  // The extraction counter once charged classifications too and kept
  // counting past the cap ("96 of 60"), so every extraction in a busy month
  // failed and its row stayed in error. The counter is fixed (usage schema
  // 2 restarts it) but the rows do not repair themselves: this re-runs each
  // failed extractable row from its kept file, in order, and stops at the
  // first genuine cap so a real budget is not spent rewriting error rows.
  const failedExtractions = useMemo(() => (documents || []).filter(d => d.status === 'error' && (d.storagePath || d.downloadUrl) && DK && DK.kindOf(d.kind) && DK.kindOf(d.kind).route === 'extract'), [documents, DK]);
  const [retrying, setRetrying] = useState(null);   // { done, total }
  const retryFailed = async () => {
    const list = failedExtractions;
    if (!list.length || retrying) return;
    setRetrying({ done: 0, total: list.length });
    let ok = 0, exhausted = false;
    for (const d of list) {
      const r = await extract(d, null, null, { quiet: true });
      if (r && r.ok) ok++;
      if (r && r.exhausted) { exhausted = true; break; }
      setRetrying(st => st ? { ...st, done: st.done + 1 } : st);
    }
    setRetrying(null);
    if (exhausted) show(`${ok} re-read; the month's extraction budget is now genuinely used up — ${list.length - ok} still failed. Add a dated ONE_TIME_ALLOWANCE month in lib/documentKinds (a bulk upload) or wait for next month.`, 'error');
    else show(`${ok} of ${list.length} failed extraction${list.length === 1 ? '' : 's'} re-read — review each before applying`);
  };

  // ── Re-reading statements for their fund positions ─────────────────────
  // Statements extracted before the fund table was part of the shape have
  // no positions. Only the newest-dated statement of each account Monarch
  // does not itemize is re-read (older ones would never be used), in order,
  // stopping at a genuine cap like the failed-row retry.
  const [rereading, setRereading] = useState(null);
  const rereadPositions = async () => {
    const list = positionRereads;
    if (!list.length || rereading) return;
    setRereading({ done: 0, total: list.length });
    let ok = 0, exhausted = false;
    for (const d of list) {
      const r = await extract(d, null, null, { quiet: true });
      if (r && r.ok) ok++;
      if (r && r.exhausted) { exhausted = true; break; }
      setRereading(st => st ? { ...st, done: st.done + 1 } : st);
    }
    setRereading(null);
    show(exhausted ? `${ok} re-read for positions; the month's extraction budget is used up — the rest wait for next month` : `${ok} of ${list.length} statement${list.length === 1 ? '' : 's'} re-read — their fund positions are on the Investments tab now`, exhausted ? 'error' : undefined);
  };

  // One file: through the repository (uploadDocumentFile: auth, preflight,
  // pending → put → uploaded), then classify and route. A failed upload is
  // reported by its title, Firebase code and the attempted path; nothing is
  // left behind for it (the provider discards its pending record).
  const processFiles = async (fileList) => {
    const files = Array.from(fileList || []);
    if (!files.length) return;
    setQueue(files.map(f => ({ name: f.name, status: 'queued' })));
    for (let i = 0; i < files.length; i++) {
      const file = files[i];
      const mark = (status, error) => setQueue(q => q.map((x, j) => j === i ? { ...x, status, error } : x));
      mark('uploading');
      let doc = null;
      try {
        doc = await uploadDocumentFile(file, { source: 'documents' });
      } catch (e) {
        mark('error', `${e.title || 'Upload failed'} — ${e.message}`);
        show(`${e.title || 'Upload failed'}: ${e.message}`, 'error');
        continue;
      }
      try {
        mark('classifying');
        const base64 = await readBase64(file);
        await classifyAndRoute(doc, base64, doc.mimeType);
        mark('done');
      } catch (e) { mark('error', e.message); }
    }
    show(`${files.length} file${files.length === 1 ? '' : 's'} processed`);
    setTimeout(() => setQueue([]), 6000);
  };

  // ── Applying an extraction ─────────────────────────────────────────────
  const APPLY_LABEL = { k401Statement: 'Apply split to the plan', insurancePolicy: 'Add to Estate policies', taxReturn: 'Save to Tax tab' };
  const apply = async (doc) => {
    setBusy(doc.id, true);
    try {
      const x = doc.extracted;
      if (doc.kind === 'k401Statement') {
        // History uploads are welcome, but an older statement must never
        // replace the split from a newer one — the date compared is the one
        // the statement PRINTS, never when it was uploaded.
        const asOf = DK.statementDateOf(doc);
        const status = (DK.appliedFieldStatus(retirementConfig, documents || []) || []).find(f => f.key === 'k401Sources');
        const inForce = status && status.inForce && status.inForce.date;
        if (!asOf) throw new Error('This statement has no statement date — set it (Re-extract) before applying');
        if (inForce && asOf < inForce && !(status.inForce && status.inForce.seeded)) throw new Error(`This statement (${asOf}) is older than the split already in force (${inForce}) — kept as history, nothing applied`);
        if (String(doc.who || '').toLowerCase() === 'spouse') throw new Error('This is the spouse\'s plan statement — its fund positions are used on the Investments tab; the money-source split is for your 401(k) only');
        // The SPLIT only (rows + the stored k401Split, together). The 401(k)
        // balance itself stays live from Monarch.
        const patch = window.TaxBuckets.applyStatementSplit(retirementConfig, { rows: x.moneySources, asOf, documentId: doc.id, fileName: doc.fileName || null });
        if (!patch) throw new Error('No money-source balances on this statement — nothing to apply');
        await saveRetirementConfig(patch);
        const r = patch.k401Split, tot = r.pretax + r.roth;
        show(`401(k) split applied from the ${asOf} statement: Roth ${fmtCur(r.roth)} of ${fmtCur(tot)} (${tot ? (r.roth / tot * 100).toFixed(1) : 0}%) — the Retirement tab reads it now`);
      } else if (doc.kind === 'insurancePolicy') {
        const KIND = { term: 'term', whole: 'whole', universal: 'whole', group: 'group', accidentalDeath: 'adnd' };
        const row = { insurer: x.insurer || '', kind: KIND[x.policyKind] || x.policyKind || 'other', insured: doc.who || (x.insured || 'you'), owner: 'insured', deathBenefit: x.deathBenefit || 0, annualPremium: x.annualPremium || null, beneficiaries: x.beneficiaries || [], expires: x.expires || '', documentId: doc.id };
        // Re-applying a document, or applying a second copy of a policy
        // already on file, updates that row instead of adding a twin — a
        // twin doubles the household's cover on paper. Matched against the
        // list as stored now, not this page's copy, which may predate a
        // removal made in another tab.
        let at = -1;
        await updateEstatePolicies(stored => {
          const policies = [...stored];
          at = policies.findIndex((p) => p && ((p.documentId && p.documentId === doc.id) || EstatePlan.duplicatePolicies([p, row])[1] === 0));
          if (at >= 0) {
            const prev = policies[at];
            policies[at] = { ...row, ...Object.fromEntries(Object.entries(prev).filter(([, v]) => v !== '' && v != null && !(Array.isArray(v) && !v.length))), documentId: prev.documentId || doc.id };
          } else policies.push(row);
          return policies;
        });
        show(at >= 0 ? `Already on the Estate tab as policy ${at + 1} — updated it rather than adding a duplicate` : 'Policy added to the Estate tab — review the insured and beneficiaries there');
      } else if (doc.kind === 'taxReturn') {
        const { taxYear, ...parsed } = x;
        await saveTaxReturn(taxYear, parsed);
        show(`${taxYear} return saved to the Tax tab`);
      }
      await saveDocument({ id: doc.id, status: 'applied', appliedAt: new Date().toISOString() });
    } catch (e) { show(e.message, 'error'); }
    setBusy(doc.id, false);
  };

  // Tie statements to the one account they are of (lib/documentReconcile
  // .identify reads doc.accountId first). Empty clears back to auto.
  const linkAccount = async (ids, accountId) => {
    try {
      for (const id of ids) await saveDocument({ id, accountId: accountId || null });
      const a = (accounts || []).find(x => x.id === accountId);
      show(accountId ? `${ids.length} statement${ids.length === 1 ? '' : 's'} linked to ${a ? (a.displayName || a.name) : 'the account'}` : 'Link cleared — matched automatically');
    } catch (e) { show(e.message, 'error'); }
  };

  const setKind = async (doc, kind) => {
    await saveDocument({ id: doc.id, kind, status: doc.extracted ? 'classified' : doc.status, extracted: null, warnings: [], appliedAt: null });
  };
  const openPdf = (doc) => window.open(doc.downloadUrl, '_blank', 'noopener');

  const lastSync = monarchAuth && monarchAuth.lastSync;
  return (
    <div className="page">
      {Toast}
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 12, flexWrap: 'wrap', marginBottom: 12 }}>
        <div className="page-title" style={{ marginBottom: 0 }}>Documents</div>
        <div style={{ fontSize: 10, color: '#475569' }}>{used} of {monthCap} extractions used in {month}{classified ? ` (${classified} classified, not capped)` : ''} · PDFs are kept in Storage · figures are applied only when you click</div>
      </div>

      {/* C5: every extracted figure against the stored one (lib/documentReconcile). */}
      {window.DocumentReconcile && window.FinanceProducts && window.FinanceProducts.DocumentReconcileCard && (
        <window.FinanceProducts.DocumentReconcileCard result={window.DocumentReconcile.reconcile({ documents, retirementConfig, accounts, netWorthSnapshots, accountBalanceHistory })} />
      )}
      {window.DocumentReconcile && window.DocumentReconcile.history && window.FinanceProducts && window.FinanceProducts.StatementHistoryCard && (
        <window.FinanceProducts.StatementHistoryCard result={window.DocumentReconcile.history({ documents, accounts, netWorthSnapshots, accountBalanceHistory, accountMap: retirementConfig && retirementConfig.accountMap, kidNames: ((kidsPlan && kidsPlan.kids) || []).map(k => k && k.name).filter(Boolean) })}
          accounts={accounts} onLink={linkAccount} />
      )}

      {/* Where each applied field came from (statement date, file), and the
          newer statement that is waiting — applied by statement date, never
          by upload order. */}
      {appliedFields.length > 0 && (
        <div className="card" style={{ marginBottom: 16 }}>
          <div className="label" style={{ marginBottom: 6 }}>Figures applied from statements</div>
          {appliedFields.map(f => {
            const latestDoc = f.latest && (documents || []).find(d => d.id === f.latest.documentId);
            return (
              <div key={f.key} style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', fontSize: 11, padding: '4px 0' }}>
                <span style={{ color: '#e2e8f0', minWidth: 180 }}>{f.label}</span>
                <span style={{ color: '#94a3b8', flex: 1 }}>
                  {f.inForce ? <>from {f.inForce.fileName || (f.inForce.seeded ? 'seeded statement rows' : f.inForce.documentId || 'stored rows')}{f.inForce.date ? <> · statement dated <b>{f.inForce.date}</b></> : ' · undated'}</> : 'not applied from any statement'}
                  {f.latest && !f.newerUnapplied && f.latest.documentId === (f.inForce && f.inForce.documentId) && <span style={{ color: '#10b981' }}> · the latest statement on file</span>}
                </span>
                {f.newerUnapplied && (
                  <>
                    <span style={{ color: '#f59e0b' }}>newer: {f.latest.fileName || f.latest.documentId} dated {f.latest.date}</span>
                    {latestDoc && <button className="btn-primary" style={{ fontSize: 10, padding: '2px 8px' }} disabled={!!busyIds[latestDoc.id]} onClick={() => apply(latestDoc)}>Apply the {f.latest.date} statement</button>}
                  </>
                )}
              </div>
            );
          })}
        </div>
      )}

      {/* Fund positions read off statements (derived/statementHoldings). */}
      {(statementCoverage.length > 0 || positionRereads.length > 0) && (
        <div className="card" style={{ marginBottom: 16 }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10, flexWrap: 'wrap', marginBottom: 4 }}>
            <div className="label" style={{ marginBottom: 0 }}>Fund positions from statements</div>
            {positionRereads.length > 0 && (
              <button className="btn-primary" style={{ fontSize: 10, padding: '2px 10px' }} disabled={!!rereading} onClick={rereadPositions}
                title={positionRereads.map(d => `${d.fileName} (${DK.statementDateOf(d)})`).join('\n')}>
                {rereading ? `Reading ${rereading.done + 1} of ${rereading.total}…` : `Read fund positions from ${positionRereads.length} statement${positionRereads.length === 1 ? '' : 's'}`}
              </button>
            )}
          </div>
          {statementCoverage.map(e => (
            <div key={e.accountIds.join('+')} style={{ fontSize: 11, color: '#94a3b8', padding: '2px 0' }}>
              <span style={{ color: '#e2e8f0' }}>{e.accountName}</span> — {e.positions.length} fund{e.positions.length === 1 ? '' : 's'} from {e.fileName || e.documentId} dated {e.statementDate}
            </div>
          ))}
          <div style={{ fontSize: 9, color: '#475569', marginTop: 4 }}>Each account uses its newest-dated statement, and only while Monarch does not itemize it — a synced position is never overwritten. Funds are valued at their statement weight × the live balance.{positionRereads.length > 0 ? ` ${positionRereads.length} latest statement${positionRereads.length === 1 ? ' was' : 's were'} read before fund positions were extracted; each re-read costs one of the month's ${monthCap} extractions.` : ''}</div>
        </div>
      )}

      {/* Drop zone */}
      <div className="card" style={{ marginBottom: 16, borderColor: dragOver ? '#10b981' : undefined, borderStyle: 'dashed', textAlign: 'center', padding: 28 }}
        onDragOver={e => { e.preventDefault(); setDragOver(true); }} onDragLeave={() => setDragOver(false)}
        onDrop={e => { e.preventDefault(); setDragOver(false); processFiles(e.dataTransfer.files); }}>
        <div style={{ fontSize: 28, marginBottom: 6 }}>📄</div>
        <div style={{ fontSize: 13, color: '#e2e8f0', fontWeight: 600 }}>Drop statements, payslips, returns or policies here</div>
        <div style={{ fontSize: 11, color: '#64748b', marginTop: 4 }}>PDF or image, up to {MAX_FILE_MB}MB each. Each file is classified, then read; payslips and tax returns go to their existing parsers.</div>
        <button className="btn-secondary" style={{ marginTop: 10, fontSize: 11 }} onClick={() => fileRef.current && fileRef.current.click()}>Choose files</button>
        <input ref={fileRef} type="file" multiple accept="application/pdf,image/*" style={{ display: 'none' }} onChange={e => { processFiles(e.target.files); e.target.value = ''; }} />
        {queue.length > 0 && <div style={{ marginTop: 10, fontSize: 11, textAlign: 'left' }}>{queue.map((q, i) => <div key={i} style={{ color: q.status === 'error' ? '#ef4444' : q.status === 'done' ? '#10b981' : '#94a3b8', wordBreak: 'break-word' }}>{q.name} — {q.status}{q.error ? `: ${q.error}` : ''}</div>)}</div>}
      </div>

      {/* Cadence */}
      {cadence && (
        <div className="card" style={{ marginBottom: 16 }}>
          <div className="label" style={{ marginBottom: 6 }}>What is current, what is overdue</div>
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill,minmax(230px,1fr))', gap: 8 }}>
            {cadence.rows.map(r => (
              <div key={r.kind} style={{ border: `1px solid ${r.overdue ? (r.never ? '#334155' : 'rgba(245,158,11,0.5)') : 'rgba(16,185,129,0.35)'}`, borderRadius: 8, padding: '8px 10px', fontSize: 11 }}>
                <div style={{ display: 'flex', justifyContent: 'space-between' }}><span style={{ color: '#e2e8f0' }}>{r.label}</span><span style={{ color: r.never ? '#64748b' : r.overdue ? '#f59e0b' : '#10b981' }}>{r.never ? 'never' : r.overdue ? 'overdue' : 'current'}</span></div>
                <div style={{ fontSize: 10, color: '#64748b', marginTop: 2 }}>{r.last ? `last ${r.last.date} (${r.ageDays}d) · due ${r.dueBy}` : `expected every ${r.cadenceDays} days`} · {r.count} on file</div>
              </div>
            ))}
          </div>
          <div style={{ fontSize: 9, color: '#475569', marginTop: 6 }}>An overdue kind also appears on the plan-completeness card and, once uploaded at least once, as an insight on the Retirement and Insights tabs.</div>
        </div>
      )}

      {/* The records */}
      <div className="card" style={{ marginBottom: 16 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 10, flexWrap: 'wrap', marginBottom: 4 }}>
          <div className="label" style={{ marginBottom: 0 }}>Uploaded ({(documents || []).length})</div>
          {failedExtractions.length > 0 && (
            <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <span style={{ fontSize: 10, color: '#64748b' }}>{failedExtractions.length} extraction{failedExtractions.length === 1 ? '' : 's'} failed · each costs one of the month's {monthCap}</span>
              <button className="btn-primary" style={{ fontSize: 10, padding: '2px 10px' }} disabled={!!retrying} onClick={retryFailed}>
                {retrying ? `Retrying ${retrying.done + 1} of ${retrying.total}…` : `Retry ${failedExtractions.length} failed`}
              </button>
            </div>
          )}
        </div>
        {!(documents || []).length && <div style={{ fontSize: 11, color: '#64748b' }}>Nothing uploaded yet.</div>}
        {(documents || []).map(doc => <DocumentRow key={doc.id} doc={doc} busy={!!busyIds[doc.id]} applied={APPLY_LABEL}
          onKind={setKind} onExtract={(d) => extract(d)} onApply={apply} onDelete={(d) => deleteDocument(d).then(() => show('Deleted')).catch(e => show(e.message, 'error'))} onOpen={openPdf}
          accounts={accounts} onLink={linkAccount} />)}
      </div>
      <div style={{ fontSize: 10, color: '#475569' }}>Data is as fresh as the last Monarch sync ({lastSync ? timeAgo(lastSync) : 'never'}); documents are as fresh as their statement dates above.</div>
    </div>
  );
}

window.FinanceViews = Object.assign(window.FinanceViews || {}, { Documents });
