// reconciliation-queue.jsx — Wave 2: Statement reconciliation + learning matcher
//
// One screen, two modes:
//   • Queue view — all unmatched income lines across statements, grouped &
//     sortable, with per-line confidence scores from the auto-matcher
//   • Match drawer — pick the right Work/Recording for one line, see candidate
//     matches with reasoning, save the decision; the matcher LEARNS from it
//
// Data flow:
//   STMT_INDEX → flatten lines → run synthAutoMatch(line) → confidence + candidates
//                                                      → user accepts or overrides
//                                                      → matchRules[] is appended
//                                                      → next pass uses those rules
//
// Exports: window.ScreenReconciliation, window.ReconciliationQueue (component)

(function () {
  const { useState, useMemo, useEffect } = React;

  // ─────────── helpers ───────────
  const fmt$ = (n) => '$' + (Number(n) || 0).toLocaleString('en-US', { maximumFractionDigits: 2, minimumFractionDigits: 2 });
  const fmtInt = (n) => (Number(n) || 0).toLocaleString('en-US');
  const norm = (s) => (s || '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
  const tokens = (s) => norm(s).split(' ').filter(Boolean);

  // Levenshtein-ish similarity (token Jaccard, fast & good enough for titles)
  function titleSim(a, b) {
    const A = new Set(tokens(a));
    const B = new Set(tokens(b));
    if (!A.size || !B.size) return 0;
    let inter = 0;
    A.forEach(t => B.has(t) && inter++);
    return inter / new Set([...A, ...B]).size;
  }

  // ─────────── auto-matcher ───────────
  // Given a line, returns { status, confidence, candidates, reasoning }
  // status: matched | uncertain | unmatched
  function autoMatch(line, ctx) {
    // Delegate to MatcherEngine if loaded (mojibake-aware + phonetic +
    // trigram + ID-partial + fractional). Falls back to legacy if not.
    if (window.MatcherEngine) {
      return window.MatcherEngine.matchOne(line, ctx);
    }
    const { recordings, works, rules } = ctx;
    const candidates = [];

    // 1) ISRC exact match → very high confidence
    if (line.isrc) {
      const r = recordings.find(x => (x.isrc || '').toUpperCase() === line.isrc.toUpperCase());
      if (r) {
        candidates.push({ kind:'recording', id:r.id, label:r.title || r.name, conf:0.99, reason:`ISRC ${line.isrc} exact match`, ref:r });
      }
    }
    // 2) UPC → release → recordings
    if (line.upc) {
      const releases = window.RELEASES || [];
      const rel = releases.find(x => (x.upc || '').replace(/\D/g,'') === line.upc.replace(/\D/g,''));
      if (rel) {
        candidates.push({ kind:'release', id:rel.id, label:rel.title || rel.name, conf:0.92, reason:`UPC ${line.upc} matches release "${rel.title||rel.name}"`, ref:rel });
      }
    }
    // 3) Saved learning rule (token signature → asset)
    const sig = makeSignature(line);
    const ruleHit = rules.find(r => r.signature === sig);
    if (ruleHit) {
      candidates.push({ kind:ruleHit.kind, id:ruleHit.id, label:ruleHit.label, conf:0.95, reason:`Learned rule: same source/title pattern matched ${ruleHit.label} ${ruleHit.appliedCount}× before`, learned:true });
    }
    // 4) Title fuzzy → works (PRO/MLC statements)
    const titleField = line.trackTitle || line.workTitle || line.title;
    if (titleField) {
      let best = null;
      for (const w of works) {
        const s = titleSim(titleField, w.title || w.name);
        if (!best || s > best.s) best = { s, w };
      }
      if (best && best.s >= 0.6) {
        candidates.push({ kind:'work', id:best.w.id, label:best.w.title || best.w.name, conf: 0.5 + best.s * 0.4, reason:`Title fuzzy match: "${titleField}" ↔ "${best.w.title||best.w.name}" (${(best.s*100).toFixed(0)}% token overlap)`, ref:best.w });
      }
    }
    // 5) Artist + title combo for recordings
    if (titleField && (line.trackArtists || line.releaseArtists)) {
      const artist = line.trackArtists || line.releaseArtists;
      let best = null;
      for (const r of recordings) {
        const ts = titleSim(titleField, r.title || r.name);
        const as = titleSim(artist, r.primaryArtistName || r.artist || '');
        const combined = ts * 0.7 + as * 0.3;
        if (!best || combined > best.s) best = { s: combined, r };
      }
      if (best && best.s >= 0.55) {
        candidates.push({ kind:'recording', id:best.r.id, label:best.r.title || best.r.name, conf: 0.4 + best.s * 0.4, reason:`Artist + title combo: "${titleField}" by ${artist} ↔ "${best.r.title||best.r.name}"`, ref:best.r });
      }
    }

    // De-dupe by id, keep highest conf
    const dedup = new Map();
    for (const c of candidates) {
      const k = c.kind + ':' + c.id;
      if (!dedup.has(k) || dedup.get(k).conf < c.conf) dedup.set(k, c);
    }
    const sorted = [...dedup.values()].sort((a,b) => b.conf - a.conf);
    const top = sorted[0];

    let status = 'unmatched';
    if (top) {
      if (top.conf >= 0.85) status = 'matched';
      else if (top.conf >= 0.55) status = 'uncertain';
    }
    return { status, confidence: top?.conf || 0, candidates: sorted, top };
  }

  // Signature: source kind + normalized title prefix + territory
  function makeSignature(line) {
    const t = norm(line.trackTitle || line.workTitle || line.title || '').slice(0, 32);
    const src = (line.sourceKind || line.source || '').toLowerCase();
    return `${src}|${t}|${line.territory || ''}`;
  }

  // ─────────── flatten statement lines into queue items ───────────
  function flattenQueue(idx) {
    const out = [];
    if (!idx || !idx.statements) return out;
    for (const stmt of idx.statements) {
      for (const ln of stmt.lines || []) {
        out.push({
          ...ln,
          stmtId: stmt.id,
          stmtSource: stmt.sourceName,
          stmtSourceColor: stmt.sourceColor,
          stmtSourceKind: stmt.sourceKind,
          stmtPeriod: stmt.periodLabel || stmt.period,
          stmtCurrency: stmt.currency || 'USD',
          amountUsd: ln.royaltyUsd || ln.amountUsd || ln.amount || 0,
          sourceKind: stmt.sourceKind,
        });
      }
    }
    return out;
  }

  // ─────────── persistent rules ───────────
  function loadRules() {
    try {
      return JSON.parse(localStorage.getItem('astro.recon.rules') || '[]');
    } catch { return []; }
  }
  function saveRules(rules) {
    try { localStorage.setItem('astro.recon.rules', JSON.stringify(rules)); } catch {}
  }

  // ═══════════════════════════════════════════════════════ MAIN SCREEN
  function ScreenReconciliation({ go }) {
    const [stmtLoaded, setStmtLoaded] = useState(!!window.__STMT_LOADED);
    useEffect(() => {
      if (stmtLoaded) return;
      const h = () => setStmtLoaded(true);
      window.addEventListener('astro-statements-loaded', h);
      return () => window.removeEventListener('astro-statements-loaded', h);
    }, [stmtLoaded]);

    const [rules, setRules] = useState(() => loadRules());
    const [overrides, setOverrides] = useState(() => {
      try { return JSON.parse(localStorage.getItem('astro.recon.overrides') || '{}'); } catch { return {}; }
    });
    const [filter, setFilter] = useState('uncertain'); // all | unmatched | uncertain | matched | mine
    const [sourceFilter, setSourceFilter] = useState('all');
    const [search, setSearch] = useState('');
    const [activeIdx, setActiveIdx] = useState(null);
    const [sortBy, setSortBy] = useState('amount-desc');

    const recordings = window.RECORDINGS || [];
    const works = window.WORKS || [];
    const stmtIdx = window.__STMT_INDEX || { statements: [] };

    const [thresholds, setThresholds] = useState(() => {
      try { return JSON.parse(localStorage.getItem('astro.recon.thresholds') || '{}'); } catch { return {}; }
    });

    const queue = useMemo(() => {
      const lines = flattenQueue(stmtIdx);
      const ctx = { recordings, works, rules, thresholds };
      // Apply manual overrides first; pass the rest through batch matcher
      // so the cluster-pass can see all the unmatched lines together.
      const annotated = lines.map(ln => ({ ...ln, key: ln.stmtId + ':' + ln.id }));
      const overridden = annotated.filter(ln => overrides[ln.key]);
      const fresh = annotated.filter(ln => !overrides[ln.key]);
      let freshResults;
      if (window.MatcherEngine) {
        freshResults = window.MatcherEngine.matchBatch(fresh, ctx);
      } else {
        freshResults = fresh.map(ln => ({ line: ln, ...autoMatch(ln, ctx) }));
      }
      const out = [];
      for (const ln of overridden) {
        const ov = overrides[ln.key];
        out.push({ ...ln, status: 'matched', confidence: 1, manual: true, match: ov, candidates: [ov], top: ov });
      }
      for (const r of freshResults) {
        out.push({ ...r.line, status: r.status, confidence: r.confidence, candidates: r.candidates, top: r.top, match: r.top || null, cluster: r.cluster });
      }
      return out;
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [stmtIdx.statements, rules, overrides, thresholds]);

    const stats = useMemo(() => ({
      total: queue.length,
      matched: queue.filter(q => q.status === 'matched').length,
      uncertain: queue.filter(q => q.status === 'uncertain').length,
      unmatched: queue.filter(q => q.status === 'unmatched').length,
      manual: queue.filter(q => q.manual).length,
      totalUsd: queue.reduce((s, q) => s + (q.amountUsd || 0), 0),
      unmatchedUsd: queue.filter(q => q.status !== 'matched').reduce((s, q) => s + (q.amountUsd || 0), 0),
    }), [queue]);

    const sources = useMemo(() => {
      const set = new Map();
      for (const q of queue) set.set(q.stmtSource, (set.get(q.stmtSource) || 0) + 1);
      return [...set.entries()].sort((a,b) => b[1] - a[1]);
    }, [queue]);

    const filtered = useMemo(() => {
      let arr = queue;
      if (filter === 'unmatched') arr = arr.filter(q => q.status === 'unmatched');
      else if (filter === 'uncertain') arr = arr.filter(q => q.status === 'uncertain');
      else if (filter === 'matched') arr = arr.filter(q => q.status === 'matched');
      else if (filter === 'mine') arr = arr.filter(q => q.manual);
      if (sourceFilter !== 'all') arr = arr.filter(q => q.stmtSource === sourceFilter);
      if (search) {
        const s = norm(search);
        arr = arr.filter(q => norm(q.trackTitle || q.workTitle || q.title || '').includes(s)
          || norm(q.trackArtists || q.releaseArtists || '').includes(s)
          || (q.isrc || '').toLowerCase().includes(s.toLowerCase()));
      }
      // Sort
      if (sortBy === 'amount-desc') arr = [...arr].sort((a,b) => (b.amountUsd||0) - (a.amountUsd||0));
      else if (sortBy === 'conf-asc') arr = [...arr].sort((a,b) => (a.confidence||0) - (b.confidence||0));
      else if (sortBy === 'title') arr = [...arr].sort((a,b) => (a.trackTitle||'').localeCompare(b.trackTitle||''));
      return arr;
    }, [queue, filter, sourceFilter, search, sortBy]);

    const active = activeIdx != null ? filtered[activeIdx] : null;

    const acceptMatch = (line, candidate) => {
      const newOverrides = { ...overrides, [line.key]: candidate };
      setOverrides(newOverrides);
      try { localStorage.setItem('astro.recon.overrides', JSON.stringify(newOverrides)); } catch {}

      // Add learning rule if this is a manual selection
      const sig = makeSignature(line);
      const existing = rules.find(r => r.signature === sig);
      if (existing) {
        existing.appliedCount = (existing.appliedCount || 1) + 1;
        existing.lastApplied = new Date().toISOString();
      } else {
        rules.push({
          signature: sig,
          kind: candidate.kind,
          id: candidate.id,
          label: candidate.label,
          createdAt: new Date().toISOString(),
          lastApplied: new Date().toISOString(),
          appliedCount: 1,
          source: line.stmtSource,
        });
      }
      const newRules = [...rules];
      setRules(newRules);
      saveRules(newRules);

      // Move to next uncertain
      const remaining = filtered.filter((_, i) => i !== activeIdx);
      if (remaining.length > 0) setActiveIdx(Math.min(activeIdx, remaining.length - 1));
      else setActiveIdx(null);
    };

    const rejectAndQuarantine = (line) => {
      const newOverrides = { ...overrides, [line.key]: { kind:'quarantine', id:'Q', label:'Quarantined — does not match my catalog', conf:1 } };
      setOverrides(newOverrides);
      try { localStorage.setItem('astro.recon.overrides', JSON.stringify(newOverrides)); } catch {}
      setActiveIdx(null);
    };

    const clearLearning = () => {
      if (!confirm('Clear all learned matching rules? Auto-suggestions will reset.')) return;
      setRules([]);
      saveRules([]);
    };

    if (!stmtLoaded) {
      return <div style={{ padding:'80px 0', textAlign:'center', color:'var(--ink-3)' }}>Loading statements…</div>;
    }

    return (
      <div>
        {/* Breadcrumb */}
        <div className="ff-mono upper" style={{ fontSize:10, color:'var(--ink-3)', letterSpacing:'.12em', marginBottom:8, display:'flex', gap:10 }}>
          <button onClick={() => go && go('royalties')} style={{ background:'transparent', border:0, padding:0, color:'inherit', cursor:'pointer', font:'inherit', letterSpacing:'inherit', textTransform:'inherit' }}>ROYALTIES</button>
          <span style={{ color:'var(--ink-4)' }}>/</span>
          <span style={{ color:'var(--ink)' }}>RECONCILIATION QUEUE</span>
        </div>

        {/* Hero */}
        <div style={{ marginBottom:24 }}>
          <h1 className="heading-swap ff-display" style={{ fontSize:'clamp(36px,4.5vw,64px)', fontWeight:700, letterSpacing:'-0.04em', lineHeight:.95, margin:0 }}>
            Reconciliation queue
          </h1>
          <p style={{ fontSize:14, color:'var(--ink-2)', maxWidth:680, margin:'14px 0 0', lineHeight:1.5 }}>
            Every income line gets matched to a Work or Recording. The matcher does this automatically where it can, and surfaces the rest here. Each manual match teaches the matcher for the next pass.
          </p>
        </div>

        {/* Stats strip */}
        <div style={{ display:'grid', gridTemplateColumns:'repeat(5, 1fr)', border:'1px solid var(--rule)', marginBottom:24 }}>
          {[
            { l:'TOTAL LINES', v:fmtInt(stats.total),                      sub: fmt$(stats.totalUsd) + ' across all statements' },
            { l:'AUTO-MATCHED', v:fmtInt(stats.matched),                   sub: ((stats.matched/(stats.total||1))*100).toFixed(1) + '% of lines', tone:'ok' },
            { l:'UNCERTAIN',    v:fmtInt(stats.uncertain),                 sub: 'needs your review',                                            tone:'accent' },
            { l:'UNMATCHED',    v:fmtInt(stats.unmatched),                 sub: 'no candidates found',                                          tone:'danger' },
            { l:'LEARNED RULES', v:fmtInt(rules.length),                   sub: rules.reduce((s, r) => s + (r.appliedCount || 0), 0) + ' applications' },
          ].map((c, i) => (
            <div key={i} style={{ padding:'18px 20px', borderRight: i < 4 ? '1px solid var(--rule)' : 'none', background: c.tone === 'accent' ? 'var(--accent)' : 'transparent', color: c.tone === 'accent' ? 'var(--accent-ink)' : 'inherit' }}>
              <div className="ff-mono upper" style={{ fontSize:9, letterSpacing:'.12em', color: c.tone === 'accent' ? 'var(--accent-ink)' : 'var(--ink-3)', marginBottom:6, opacity: c.tone === 'accent' ? .8 : 1 }}>{c.l}</div>
              <div className="ff-display num" style={{ fontSize:26, fontWeight:600, letterSpacing:'-0.02em' }}>{c.v}</div>
              <div className="ff-mono" style={{ fontSize:10, color: c.tone === 'accent' ? 'var(--accent-ink)' : 'var(--ink-3)', marginTop:4, opacity:.8 }}>{c.sub}</div>
            </div>
          ))}
        </div>

        {/* Toolbar */}
        <div style={{ display:'flex', gap:10, marginBottom:14, alignItems:'center', flexWrap:'wrap' }}>
          {/* Status filter */}
          <div style={{ display:'flex', gap:0, border:'1px solid var(--ink)' }}>
            {['uncertain','unmatched','matched','mine','all'].map(f => (
              <button key={f} onClick={() => { setFilter(f); setActiveIdx(null); }}
                className="ff-mono upper"
                style={{ padding:'8px 12px', fontSize:10, letterSpacing:'.08em', fontWeight:600, border:0, background: filter === f ? 'var(--ink)' : 'transparent', color: filter === f ? 'var(--bg)' : 'var(--ink)', cursor:'pointer' }}>
                {f === 'mine' ? 'My matches' : f}
              </button>
            ))}
          </div>

          <select value={sourceFilter} onChange={e => setSourceFilter(e.target.value)} className="ff-mono"
            style={{ padding:'8px 10px', fontSize:11, border:'1px solid var(--rule)', background:'var(--paper)' }}>
            <option value="all">All sources</option>
            {sources.map(([n, c]) => <option key={n} value={n}>{n} ({c})</option>)}
          </select>

          <input value={search} onChange={e => setSearch(e.target.value)} placeholder="Search title / artist / ISRC…"
            className="ff-mono"
            style={{ flex:1, minWidth:240, padding:'8px 10px', fontSize:11, border:'1px solid var(--rule)', background:'var(--paper)' }}/>

          <select value={sortBy} onChange={e => setSortBy(e.target.value)} className="ff-mono"
            style={{ padding:'8px 10px', fontSize:11, border:'1px solid var(--rule)', background:'var(--paper)' }}>
            <option value="amount-desc">Amount ↓</option>
            <option value="conf-asc">Confidence ↑</option>
            <option value="title">Title A→Z</option>
          </select>

          <button onClick={clearLearning} className="ff-mono upper" disabled={rules.length === 0}
            style={{ padding:'8px 12px', fontSize:10, letterSpacing:'.08em', fontWeight:600, border:'1px solid var(--rule)', background:'transparent', cursor: rules.length ? 'pointer' : 'not-allowed', opacity: rules.length ? 1 : .5 }}>
            CLEAR LEARNING
          </button>
        </div>

        {/* Two-pane: list + drawer */}
        <div style={{ display:'grid', gridTemplateColumns: active ? '1fr 480px' : '1fr', gap:0, border:'1px solid var(--rule)', minHeight:600 }}>
          {/* List */}
          <div style={{ borderRight: active ? '1px solid var(--rule)' : 'none', overflow:'auto', maxHeight:'calc(100vh - 320px)' }}>
            <div className="ff-mono upper" style={{ display:'grid', gridTemplateColumns:'70px 1fr 110px 70px 110px 90px', gap:12, padding:'10px 14px', fontSize:9, color:'var(--ink-3)', background:'var(--bg-2)', borderBottom:'1px solid var(--rule)', position:'sticky', top:0, letterSpacing:'.1em' }}>
              <span>STATUS</span><span>LINE</span><span>SOURCE</span><span style={{ textAlign:'right' }}>CONF</span><span style={{ textAlign:'right' }}>AMOUNT</span><span style={{ textAlign:'right' }}></span>
            </div>
            {filtered.length === 0 && (
              <div style={{ padding:'60px 20px', textAlign:'center', color:'var(--ink-3)' }}>
                <div className="ff-display" style={{ fontSize:20, fontWeight:600, marginBottom:8 }}>Nothing here</div>
                <div style={{ fontSize:13 }}>Adjust filters or check another tab.</div>
              </div>
            )}
            {filtered.slice(0, 200).map((q, i) => (
              <QueueRow key={q.key} q={q} active={activeIdx === i} onClick={() => setActiveIdx(i)} />
            ))}
            {filtered.length > 200 && (
              <div style={{ padding:'14px', textAlign:'center', color:'var(--ink-3)', borderTop:'1px solid var(--rule-soft)' }}>
                <span className="ff-mono" style={{ fontSize:11 }}>Showing 200 of {fmtInt(filtered.length)} — narrow with filters</span>
              </div>
            )}
          </div>

          {/* Drawer */}
          {active && (
            <MatchDrawer
              line={active}
              onClose={() => setActiveIdx(null)}
              onAccept={(c) => acceptMatch(active, c)}
              onReject={() => rejectAndQuarantine(active)}
              recordings={recordings}
              works={works}
            />
          )}
        </div>

        {/* Learning panel */}
        {rules.length > 0 && (
          <LearningPanel rules={rules} />
        )}

        {/* Calibration panel */}
        <CalibrationPanel queue={queue} thresholds={thresholds} setThresholds={(t) => {
          setThresholds(t);
          try { localStorage.setItem('astro.recon.thresholds', JSON.stringify(t)); } catch {}
        }} />
      </div>
    );
  }

  // ─────────── Queue row ───────────
  function QueueRow({ q, active, onClick }) {
    const title = q.trackTitle || q.workTitle || q.title || '—';
    const artist = q.trackArtists || q.releaseArtists || '';
    const tone = q.status === 'matched' ? 'ok' : q.status === 'uncertain' ? 'accent' : 'danger';
    const dotColor = tone === 'ok' ? '#33d97a' : tone === 'accent' ? 'var(--accent)' : '#e94b4b';
    return (
      <div onClick={onClick}
        style={{ display:'grid', gridTemplateColumns:'70px 1fr 110px 70px 110px 90px', gap:12, padding:'12px 14px', borderBottom:'1px solid var(--rule-soft)', cursor:'pointer', background: active ? 'var(--bg-2)' : 'transparent', alignItems:'center' }}>
        <div style={{ display:'flex', gap:6, alignItems:'center' }}>
          <span style={{ width:7, height:7, background:dotColor, flexShrink:0, borderRadius: '50%' }}/>
          <span className="ff-mono upper" style={{ fontSize:9, letterSpacing:'.08em', color:'var(--ink-2)', fontWeight:600 }}>
            {q.manual ? 'MINE' : q.status.slice(0,5)}
          </span>
        </div>
        <div style={{ minWidth:0 }}>
          <div style={{ fontSize:13, fontWeight:500, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>{title}</div>
          <div className="ff-mono" style={{ fontSize:10, color:'var(--ink-3)', marginTop:2, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' }}>
            {artist && <>{artist} · </>}
            {q.isrc && <>{q.isrc} · </>}
            {q.territory || q.dsp}
          </div>
          {q.match && q.status !== 'unmatched' && (
            <div className="ff-mono" style={{ fontSize:10, color:'var(--ink-2)', marginTop:3, display:'inline-flex', gap:4, alignItems:'center' }}>
              <span style={{ color:'var(--ink-3)' }}>↳</span>
              {q.match.label}
              {q.match.learned && <span style={{ fontSize:8, padding:'1px 4px', background:'var(--ink)', color:'var(--bg)', letterSpacing:'.08em' }}>LEARNED</span>}
              {q.match.strategy === 'cluster' && <span style={{ fontSize:8, padding:'1px 4px', background:'var(--accent)', color:'var(--accent-ink)', letterSpacing:'.08em' }}>CLUSTER</span>}
              {q.match.strategy === 'fractional' && <span style={{ fontSize:8, padding:'1px 4px', background:'var(--ink-2)', color:'var(--bg)', letterSpacing:'.08em' }}>SHARE</span>}
              {q.match.strategy === 'iswc' && <span style={{ fontSize:8, padding:'1px 4px', background:'#1f7a3a', color:'#fff', letterSpacing:'.08em' }}>ISWC</span>}
            </div>
          )}
        </div>
        <span className="ff-mono upper" style={{ fontSize:10, color:'var(--ink-3)', letterSpacing:'.08em' }}>{q.stmtSource}</span>
        <span className="ff-mono num" style={{ fontSize:11, textAlign:'right', color: q.confidence > 0.85 ? 'var(--ink)' : q.confidence > 0.5 ? 'var(--ink-2)' : 'var(--ink-3)', fontWeight: q.confidence > 0.85 ? 600 : 400 }}>
          {q.confidence > 0 ? (q.confidence*100).toFixed(0) + '%' : '—'}
        </span>
        <span className="ff-mono num" style={{ fontSize:13, fontWeight:600, textAlign:'right' }}>{fmt$(q.amountUsd)}</span>
        <span style={{ textAlign:'right' }}>
          <span className="ff-mono upper" style={{ fontSize:9, letterSpacing:'.08em', color:'var(--ink-3)' }}>{q.stmtPeriod}</span>
        </span>
      </div>
    );
  }

  // ─────────── Match drawer ───────────
  function MatchDrawer({ line, onClose, onAccept, onReject, recordings, works }) {
    const [showAll, setShowAll] = useState(false);

    // If unmatched, generate alt candidates by relaxed search
    const altCandidates = useMemo(() => {
      if (line.status !== 'unmatched' && !showAll) return [];
      const title = line.trackTitle || line.workTitle || line.title || '';
      if (!title) return [];
      const out = [];
      for (const r of recordings.slice(0, 600)) {
        const s = titleSim(title, r.title || r.name);
        if (s >= 0.3) out.push({ kind:'recording', id:r.id, label:r.title || r.name, conf:s, reason:`Token similarity ${(s*100).toFixed(0)}%`, ref:r });
      }
      for (const w of works.slice(0, 600)) {
        const s = titleSim(title, w.title || w.name);
        if (s >= 0.3) out.push({ kind:'work', id:w.id, label:w.title || w.name, conf:s, reason:`Token similarity ${(s*100).toFixed(0)}%`, ref:w });
      }
      return out.sort((a,b) => b.conf - a.conf).slice(0, 8);
    }, [line, showAll, recordings, works]);

    const candidates = (line.candidates && line.candidates.length > 0) ? line.candidates : altCandidates;

    return (
      <div style={{ display:'flex', flexDirection:'column', overflow:'auto', maxHeight:'calc(100vh - 320px)' }}>
        {/* Drawer header */}
        <div style={{ padding:'18px 20px', borderBottom:'1px solid var(--rule)', display:'flex', justifyContent:'space-between', alignItems:'flex-start', gap:14 }}>
          <div style={{ minWidth:0 }}>
            <div className="ff-mono upper" style={{ fontSize:9, color:'var(--ink-3)', letterSpacing:'.12em', marginBottom:6 }}>INCOME LINE</div>
            <div style={{ fontSize:18, fontWeight:600, letterSpacing:'-0.01em', overflow:'hidden', textOverflow:'ellipsis' }}>
              {line.trackTitle || line.workTitle || line.title || '(no title)'}
            </div>
            {(line.trackArtists || line.releaseArtists) && (
              <div style={{ fontSize:13, color:'var(--ink-2)', marginTop:3 }}>
                {line.trackArtists || line.releaseArtists}
              </div>
            )}
          </div>
          <button onClick={onClose} style={{ background:'transparent', border:0, fontSize:20, cursor:'pointer', color:'var(--ink-3)', padding:0 }}>×</button>
        </div>

        {/* Line attributes */}
        <div style={{ padding:'14px 20px', borderBottom:'1px solid var(--rule)', display:'grid', gridTemplateColumns:'1fr 1fr', gap:10 }}>
          {[
            ['Source', line.stmtSource],
            ['Period', line.stmtPeriod],
            ['Amount', fmt$(line.amountUsd)],
            ['DSP', line.dsp],
            ['Territory', line.territory],
            ['ISRC', line.isrc],
            ['UPC', line.upc],
            ['Catalogue', line.catalogue],
          ].filter(([,v]) => v).map(([k, v]) => (
            <div key={k}>
              <div className="ff-mono upper" style={{ fontSize:9, color:'var(--ink-3)', letterSpacing:'.1em' }}>{k}</div>
              <div className="ff-mono" style={{ fontSize:11, marginTop:2 }}>{v}</div>
            </div>
          ))}
        </div>

        {/* Candidates */}
        <div style={{ padding:'18px 20px', flex:1 }}>
          <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', marginBottom:14 }}>
            <div className="ff-mono upper" style={{ fontSize:10, color:'var(--ink-2)', letterSpacing:'.12em', fontWeight:600 }}>
              {line.status === 'matched' ? 'AUTO-MATCH (CONFIRM)' : line.status === 'uncertain' ? 'CANDIDATES — PICK ONE' : 'NO STRONG MATCHES'}
            </div>
            <button onClick={() => setShowAll(s => !s)} className="ff-mono upper" style={{ padding:'4px 8px', fontSize:9, letterSpacing:'.08em', border:'1px solid var(--rule)', background:'transparent', cursor:'pointer' }}>
              {showAll ? 'TOP MATCHES' : 'SEARCH MORE'}
            </button>
          </div>

          {candidates.length === 0 && (
            <div style={{ padding:'30px 20px', textAlign:'center', border:'1px dashed var(--rule)', color:'var(--ink-3)', fontSize:12 }}>
              No catalog candidates. Try widening search or quarantine for the next pass.
            </div>
          )}

          {candidates.map((c, i) => (
            <div key={c.kind + ':' + c.id + i} style={{ padding:'14px', border: i === 0 && line.status === 'matched' ? '2px solid var(--ink)' : '1px solid var(--rule-soft)', marginBottom:8, cursor:'pointer', background: i === 0 && line.status === 'matched' ? 'var(--bg-2)' : 'transparent' }}
              onClick={() => onAccept(c)}>
              <div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', marginBottom:6 }}>
                <div style={{ display:'flex', gap:8, alignItems:'center', minWidth:0 }}>
                  <span className="ff-mono upper" style={{ fontSize:9, padding:'2px 5px', background:'var(--bg-2)', border:'1px solid var(--rule)', letterSpacing:'.1em' }}>{c.kind}</span>
                  <span style={{ fontSize:13, fontWeight:600, overflow:'hidden', textOverflow:'ellipsis' }}>{c.label}</span>
                  {c.learned && <span className="ff-mono upper" style={{ fontSize:8, padding:'1px 4px', background:'var(--ink)', color:'var(--bg)', letterSpacing:'.1em', flexShrink:0 }}>LEARNED</span>}
                </div>
                <span className="ff-mono num" style={{ fontSize:11, fontWeight:600, color: c.conf > 0.85 ? '#1f7a3a' : c.conf > 0.6 ? 'var(--ink)' : 'var(--ink-2)', flexShrink:0, marginLeft:10 }}>
                  {(c.conf*100).toFixed(0)}%
                </span>
              </div>
              <div className="ff-mono" style={{ fontSize:10, color:'var(--ink-3)', lineHeight:1.5 }}>
                {c.reason}
              </div>
              {/* Confidence bar */}
              <div style={{ marginTop:8, height:4, background:'var(--bg-2)' }}>
                <div style={{ width:`${c.conf*100}%`, height:'100%', background: c.conf > 0.85 ? '#33d97a' : c.conf > 0.5 ? 'var(--accent)' : 'var(--ink-3)' }}/>
              </div>
            </div>
          ))}
        </div>

        {/* Drawer actions */}
        <div style={{ padding:'14px 20px', borderTop:'1px solid var(--rule)', display:'flex', gap:8, background:'var(--bg-2)' }}>
          <button onClick={onReject} className="ff-mono upper"
            style={{ padding:'10px 14px', fontSize:10, letterSpacing:'.08em', fontWeight:600, border:'1px solid var(--rule)', background:'transparent', cursor:'pointer', flex:1 }}>
            QUARANTINE — NOT MINE
          </button>
          {candidates[0] && (
            <button onClick={() => onAccept(candidates[0])} className="ff-mono upper"
              style={{ padding:'10px 14px', fontSize:10, letterSpacing:'.08em', fontWeight:600, border:0, background:'var(--ink)', color:'var(--bg)', cursor:'pointer', flex:1 }}>
              ACCEPT TOP MATCH ↵
            </button>
          )}
        </div>
      </div>
    );
  }

  // ─────────── Learning panel ───────────
  function LearningPanel({ rules }) {
    const [expanded, setExpanded] = useState(false);
    const sorted = [...rules].sort((a,b) => (b.appliedCount||0) - (a.appliedCount||0));
    const shown = expanded ? sorted : sorted.slice(0, 5);

    return (
      <div style={{ marginTop:32, border:'1px solid var(--rule)' }}>
        <div style={{ padding:'14px 18px', borderBottom:'1px solid var(--rule)', display:'flex', justifyContent:'space-between', alignItems:'center', background:'var(--bg-2)' }}>
          <div style={{ display:'flex', gap:10, alignItems:'baseline' }}>
            <span className="ff-mono upper" style={{ fontSize:10, color:'var(--ink-3)', letterSpacing:'.12em' }}>LEARNED MATCHING RULES</span>
            <span className="ff-mono num" style={{ fontSize:13, fontWeight:600 }}>{rules.length}</span>
          </div>
          <button onClick={() => setExpanded(e => !e)} className="ff-mono upper" style={{ padding:'4px 8px', fontSize:9, letterSpacing:'.08em', border:'1px solid var(--rule)', background:'transparent', cursor:'pointer' }}>
            {expanded ? 'COLLAPSE' : `SHOW ALL (${rules.length})`}
          </button>
        </div>
        <div className="ff-mono upper" style={{ display:'grid', gridTemplateColumns:'120px 1fr 1fr 80px 110px', gap:12, padding:'10px 18px', fontSize:9, color:'var(--ink-3)', borderBottom:'1px solid var(--rule-soft)', letterSpacing:'.1em' }}>
          <span>SOURCE</span><span>SIGNATURE</span><span>RESOLVES TO</span><span style={{ textAlign:'right' }}>USES</span><span style={{ textAlign:'right' }}>LAST APPLIED</span>
        </div>
        {shown.map((r, i) => (
          <div key={i} style={{ display:'grid', gridTemplateColumns:'120px 1fr 1fr 80px 110px', gap:12, padding:'10px 18px', borderBottom:'1px solid var(--rule-soft)', alignItems:'center' }}>
            <span className="ff-mono upper" style={{ fontSize:10, color:'var(--ink-2)', letterSpacing:'.08em' }}>{r.source || '—'}</span>
            <span className="ff-mono" style={{ fontSize:10, color:'var(--ink-3)', overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>{r.signature.split('|').slice(1,2).join(' ').slice(0, 40) || '—'}</span>
            <span style={{ fontSize:12, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap' }}>
              <span className="ff-mono upper" style={{ fontSize:9, color:'var(--ink-3)', marginRight:6, letterSpacing:'.08em' }}>{r.kind}</span>
              {r.label}
            </span>
            <span className="ff-mono num" style={{ fontSize:11, textAlign:'right', fontWeight:600 }}>{r.appliedCount || 1}×</span>
            <span className="ff-mono num" style={{ fontSize:10, textAlign:'right', color:'var(--ink-3)' }}>{r.lastApplied ? r.lastApplied.slice(0, 10) : '—'}</span>
          </div>
        ))}
      </div>
    );
  }

  // ─────────── Calibration panel ───────────
  function CalibrationPanel({ queue, thresholds, setThresholds }) {
    const calib = useMemo(() => {
      if (!window.MatcherEngine) return [];
      const results = queue.map(q => ({ line: q, status: q.status, confidence: q.confidence, top: q.top, manual: q.manual }));
      return window.MatcherEngine.calibrate(results);
    }, [queue]);

    const [stratStats, clusterCount] = useMemo(() => {
      const s = new Map();
      let cl = 0;
      for (const q of queue) {
        if (q.cluster) cl++;
        const strat = q.top?.strategy || (q.status === 'unmatched' ? 'none' : 'unknown');
        s.set(strat, (s.get(strat) || 0) + 1);
      }
      return [[...s.entries()].sort((a, b) => b[1] - a[1]), cl];
    }, [queue]);

    if (calib.length === 0) return null;

    const STRATEGY_LABEL = {
      isrc:'ISRC exact', 'isrc-partial':'ISRC partial', upc:'UPC exact', iswc:'ISWC partial',
      learned:'Learned rule', 'title-fuzzy':'Title fuzzy', 'artist-title':'Artist + title',
      fractional:'Fractional split', cluster:'Cluster pass', none:'No candidate', unknown:'Other',
    };

    const updateThreshold = (sourceKey, value) => {
      const next = { ...thresholds, [sourceKey]: { ...(thresholds[sourceKey] || {}), acceptAt: value, reviewAt: Math.max(0.4, value - 0.3) } };
      setThresholds(next);
    };

    return (
      <div style={{ marginTop:32, border:'1px solid var(--rule)' }}>
        <div style={{ padding:'14px 18px', borderBottom:'1px solid var(--rule)', background:'var(--bg-2)', display:'flex', justifyContent:'space-between', alignItems:'baseline', gap:14 }}>
          <div>
            <div className="ff-mono upper" style={{ fontSize:10, color:'var(--ink-3)', letterSpacing:'.12em' }}>CONFIDENCE CALIBRATION · PER SOURCE</div>
            <div style={{ fontSize:12, color:'var(--ink-2)', marginTop:4, lineHeight:1.5 }}>
              Histogram of match confidence by source. Drag to set the auto-apply threshold; lines at or above are auto-matched, below are surfaced for review.
            </div>
          </div>
          <div className="ff-mono" style={{ fontSize:11, color:'var(--ink-3)', textAlign:'right', flexShrink:0 }}>
            <div>{clusterCount} cluster-boosted</div>
            <div style={{ marginTop:2 }}>{stratStats.length} strategies firing</div>
          </div>
        </div>

        {/* Strategy breakdown */}
        <div style={{ display:'flex', gap:0, padding:'12px 18px', borderBottom:'1px solid var(--rule-soft)', flexWrap:'wrap' }}>
          {stratStats.map(([k, v]) => (
            <div key={k} style={{ padding:'4px 14px 4px 0', marginRight:14, borderRight:'1px solid var(--rule-soft)' }}>
              <div className="ff-mono upper" style={{ fontSize:9, color:'var(--ink-3)', letterSpacing:'.08em' }}>{STRATEGY_LABEL[k] || k}</div>
              <div className="ff-mono num" style={{ fontSize:13, fontWeight:600 }}>{fmtInt(v)}</div>
            </div>
          ))}
        </div>

        {/* Per-source histogram */}
        <div style={{ padding:'8px 0' }}>
          {calib.map((c) => {
            const max = Math.max(1, ...c.hist);
            const accept = thresholds[c.source]?.acceptAt ?? c.suggestedAcceptAt;
            return (
              <div key={c.source} style={{ display:'grid', gridTemplateColumns:'180px 1fr 240px', gap:18, alignItems:'center', padding:'12px 18px', borderBottom:'1px solid var(--rule-soft)' }}>
                <div>
                  <div className="ff-mono upper" style={{ fontSize:10, color:'var(--ink-2)', letterSpacing:'.08em', fontWeight:600 }}>{c.source}</div>
                  <div className="ff-mono" style={{ fontSize:10, color:'var(--ink-3)', marginTop:3 }}>
                    {fmtInt(c.n)} lines · avg {(c.avg*100).toFixed(0)}%
                  </div>
                </div>
                {/* histogram with threshold marker */}
                <div style={{ position:'relative', height:38 }}>
                  <div style={{ display:'flex', alignItems:'flex-end', gap:2, height:'100%' }}>
                    {c.hist.map((v, i) => {
                      const bAccept = (i + 1) / 10 > accept - 1e-6;
                      return (
                        <div key={i} style={{ flex:1, position:'relative', height:'100%' }}>
                          <div style={{
                            position:'absolute', bottom:0, left:0, right:0,
                            height: `${(v / max) * 100}%`,
                            background: bAccept ? '#33d97a' : 'var(--ink-3)',
                            opacity: 0.85,
                          }} title={`${(i*10)}–${(i+1)*10}% conf · ${v} lines`} />
                        </div>
                      );
                    })}
                  </div>
                  <div style={{
                    position:'absolute', top:-4, bottom:-4, left: `${accept*100}%`,
                    width: 0, borderLeft: '2px dashed var(--ink)',
                    pointerEvents:'none',
                  }}/>
                </div>
                <div style={{ display:'flex', alignItems:'center', gap:8 }}>
                  <span className="ff-mono upper" style={{ fontSize:9, color:'var(--ink-3)', letterSpacing:'.08em', flexShrink:0 }}>ACCEPT ≥</span>
                  <input type="range" min="0.5" max="1" step="0.01" value={accept}
                    onChange={(e) => updateThreshold(c.source, parseFloat(e.target.value))}
                    style={{ flex:1 }}/>
                  <span className="ff-mono num" style={{ fontSize:11, fontWeight:600, width:40, textAlign:'right' }}>{(accept*100).toFixed(0)}%</span>
                  {Math.abs(accept - c.suggestedAcceptAt) > 0.01 && (
                    <button onClick={() => updateThreshold(c.source, c.suggestedAcceptAt)}
                      className="ff-mono upper"
                      style={{ padding:'3px 6px', fontSize:8, letterSpacing:'.08em', border:'1px solid var(--rule)', background:'transparent', cursor:'pointer', flexShrink:0 }}
                      title={`Suggested: ${(c.suggestedAcceptAt*100).toFixed(0)}%`}>
                      RESET
                    </button>
                  )}
                </div>
              </div>
            );
          })}
        </div>
      </div>
    );
  }

  Object.assign(window, { ScreenReconciliation });
})();

