// dedup.jsx — catalog dedup screen
//
// Sweeps WORKS, RECORDINGS, RELEASES, PARTIES looking for likely duplicate
// pairs using fuzzy-engine.jsx. Three tabs:
//   01 · Candidates  — ranked pair list w/ algorithm breakdown + merge action
//   02 · Blocking    — explains the blocking strategy and lets user tune it
//   03 · Merge log   — every merge is reversible, recorded, replayable
//
// EXPORT: window.ScreenDedup
// ============================================================================
(() => {
  const { useState, useMemo, useEffect } = React;
  const F = () => window.FuzzyEngine;

  const KINDS = [
    { v: 'works',      l: 'Works',      src: () => window.WORKS || [],      get: (w) => w.title || w.name },
    { v: 'recordings', l: 'Recordings', src: () => window.RECORDINGS || [], get: (r) => r.title || r.name },
    { v: 'releases',   l: 'Releases',   src: () => window.RELEASES || [],   get: (r) => r.title || r.name },
    { v: 'parties',    l: 'Parties',    src: () => window.PARTIES || [],    get: (p) => p.name || p.legalName },
  ];

  // ── merge log persistence ────────────────────────────────────────
  const MERGE_KEY = 'astro.dedup.merges';
  function loadMerges() {
    try { return JSON.parse(localStorage.getItem(MERGE_KEY) || '[]'); }
    catch (_) { return []; }
  }
  function saveMerges(arr) {
    try { localStorage.setItem(MERGE_KEY, JSON.stringify(arr.slice(0, 500))); }
    catch (_) {}
  }

  function ScreenDedup({ go, payload }) {
    const [tab, setTab] = useState(payload?.tab || 'candidates');
    const [kind, setKind] = useState(payload?.kind || 'works');
    const [threshold, setThreshold] = useState(0.78);
    const [pairs, setPairs] = useState(null);
    const [scanning, setScanning] = useState(false);
    const [scanMs, setScanMs] = useState(0);
    const [inspect, setInspect] = useState(null);
    const [merges, setMerges] = useState(loadMerges);
    const [dismissed, setDismissed] = useState(new Set());

    function runScan() {
      if (!F()) return;
      setScanning(true);
      setPairs(null);
      // Defer to next frame so the UI flips before the (potentially heavy)
      // scoring loop runs.
      setTimeout(() => {
        const k = KINDS.find(x => x.v === kind);
        if (!k) { setScanning(false); return; }
        const items = k.src();
        const t0 = performance.now();
        // build corpus DF for soft-tfidf signal
        try { F().buildCorpusDFFromCatalog(); } catch (_) {}
        const found = F().findDuplicates(items, k.get, { threshold, maxPairs: 300 });
        setScanMs(Math.round(performance.now() - t0));
        setPairs(found);
        setScanning(false);
      }, 30);
    }

    // Auto-scan when entering candidates tab
    useEffect(() => {
      if (tab === 'candidates' && !pairs && !scanning) runScan();
    }, [tab, kind]);

    function dismiss(pair) {
      const id = pairId(pair);
      setDismissed(s => { const n = new Set(s); n.add(id); return n; });
    }

    function recordMerge(pair, keep) {
      const k = KINDS.find(x => x.v === kind);
      const drop = pair.a === keep ? pair.b : pair.a;
      const entry = {
        id: 'mrg_' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
        ts: Date.now(),
        kind,
        keepId: keep.id, keepLabel: k.get(keep),
        dropId: drop.id, dropLabel: k.get(drop),
        score: pair.score,
      };
      const next = [entry, ...merges];
      setMerges(next);
      saveMerges(next);
      setInspect(null);
      dismiss(pair);
    }

    function undoMerge(entry) {
      if (!confirm(`Undo merge — restore "${entry.dropLabel}" as a separate ${entry.kind.slice(0, -1)}?`)) return;
      const next = merges.filter(m => m.id !== entry.id);
      setMerges(next);
      saveMerges(next);
    }

    const visiblePairs = useMemo(() => {
      if (!pairs) return [];
      return pairs.filter(p => !dismissed.has(pairId(p)));
    }, [pairs, dismissed]);

    const TABS = [
      { id: 'candidates', l: '01 · Candidates' },
      { id: 'blocking',   l: '02 · Blocking' },
      { id: 'log',        l: '03 · Merge log' },
    ];

    return (
      <div style={{ padding: '24px 32px 80px', minHeight: '100vh', background: 'var(--bg)' }}>
        {/* 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('dashboard')} style={{ background: 'transparent', border: 0, padding: 0, color: 'inherit', cursor: 'pointer', font: 'inherit', letterSpacing: 'inherit', textTransform: 'inherit' }}>TOOLS</button>
          <span style={{ color: 'var(--ink-4)' }}>/</span>
          <span style={{ color: 'var(--ink)' }}>CATALOG DEDUP</span>
        </div>

        {/* hero */}
        <div style={{ display: 'flex', alignItems: 'flex-end', gap: 16, marginBottom: 8 }}>
          <div style={{ flex: 1 }}>
            <div className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.12em', color: 'var(--ink-3)', fontWeight: 600 }}>TOOLS · QUALITY</div>
            <h1 style={{ fontSize: 28, fontWeight: 400, margin: '4px 0 4px', letterSpacing: '-.01em' }}>Catalog dedup</h1>
            <div style={{ fontSize: 13, color: 'var(--ink-3)', maxWidth: 820 }}>
              Find likely-duplicate works, recordings, releases, and parties across the catalog. Six fuzzy algorithms run in parallel, blocked by phonetic key + leading trigram so even a 50k-row sweep stays cheap. Every merge is reversible.
            </div>
          </div>
        </div>

        {/* tabs */}
        <div style={{ display: 'flex', gap: 0, borderBottom: '2px solid var(--ink)', margin: '24px 0 28px' }}>
          {TABS.map(t => (
            <button key={t.id} onClick={() => setTab(t.id)} className="ff-mono upper" style={{
              background: tab === t.id ? 'var(--ink)' : 'transparent',
              color: tab === t.id ? 'var(--bg)' : 'var(--ink-2)',
              border: 0, padding: '10px 18px', fontSize: 11, letterSpacing: '.08em',
              cursor: 'pointer', fontWeight: tab === t.id ? 600 : 500,
            }}>{t.l}</button>
          ))}
          <span style={{ flex: 1 }} />
          {tab === 'candidates' && (
            <div style={{ display: 'flex', gap: 8, alignItems: 'center', paddingBottom: 4 }}>
              <span className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em' }}>SCAN</span>
              <select value={kind} onChange={e => { setKind(e.target.value); setPairs(null); setDismissed(new Set()); }}
                className="ff-mono" style={{ fontSize: 11, padding: '4px 8px', background: 'var(--paper)', border: '1px solid var(--rule)', color: 'var(--ink)' }}>
                {KINDS.map(k => <option key={k.v} value={k.v}>{k.l}</option>)}
              </select>
              <span className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em', marginLeft: 8 }}>≥</span>
              <input type="number" min="0.5" max="0.99" step="0.01" value={threshold}
                onChange={e => setThreshold(parseFloat(e.target.value) || 0.78)}
                className="ff-mono num" style={{ width: 60, fontSize: 11, padding: '4px 6px', background: 'var(--paper)', border: '1px solid var(--rule)', color: 'var(--ink)', textAlign: 'right' }} />
              <button onClick={runScan} disabled={scanning} className="ff-mono upper" style={{
                fontSize: 10, letterSpacing: '.1em', padding: '5px 12px', background: 'var(--ink)', color: 'var(--bg)',
                border: 0, cursor: scanning ? 'wait' : 'pointer', fontWeight: 600, marginLeft: 4,
              }}>{scanning ? 'Scanning…' : 'Run scan'}</button>
            </div>
          )}
        </div>

        {tab === 'candidates' && (
          <Candidates pairs={visiblePairs} totalRaw={pairs?.length || 0} dismissedCount={dismissed.size}
            scanning={scanning} kind={kind} scanMs={scanMs}
            onInspect={setInspect} onDismiss={dismiss} go={go} />
        )}
        {tab === 'blocking' && <BlockingExplainer />}
        {tab === 'log' && <MergeLog merges={merges} onUndo={undoMerge} go={go} />}

        {inspect && (
          <InspectDrawer pair={inspect} kind={kind}
            onClose={() => setInspect(null)}
            onMerge={recordMerge}
            onDismiss={(p) => { dismiss(p); setInspect(null); }} />
        )}
      </div>
    );
  }

  function pairId(p) { return (p.a.id || '') + '::' + (p.b.id || ''); }

  // ── 01 · Candidates ───────────────────────────────────────────────
  function Candidates({ pairs, totalRaw, dismissedCount, scanning, kind, scanMs, onInspect, onDismiss, go }) {
    const k = KINDS.find(x => x.v === kind);
    if (scanning) {
      return <div style={{ padding: '80px 0', textAlign: 'center', color: 'var(--ink-3)' }}>Scanning catalog…</div>;
    }
    if (!pairs || pairs.length === 0) {
      return (
        <div style={{ padding: '60px 0', textAlign: 'center' }}>
          <div className="ff-mono upper" style={{ fontSize: 10, color: 'var(--ink-3)', letterSpacing: '.12em', marginBottom: 8 }}>NO DUPLICATES FOUND</div>
          <div style={{ fontSize: 14, color: 'var(--ink-2)' }}>
            {totalRaw === 0
              ? `No likely-duplicate ${k?.l.toLowerCase()} at this threshold. Lower the score floor to widen the search.`
              : `All ${dismissedCount} candidate${dismissedCount === 1 ? '' : 's'} dismissed.`}
          </div>
        </div>
      );
    }
    return (
      <div>
        {/* KPI strip */}
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', borderTop: '1px solid var(--rule)', borderBottom: '1px solid var(--rule)', marginBottom: 16 }}>
          <Kpi label="CANDIDATES" value={pairs.length} />
          <Kpi label="HIGH CONF (≥.92)" value={pairs.filter(p => p.score >= 0.92).length} tone="warn" />
          <Kpi label="DISMISSED" value={dismissedCount} />
          <Kpi label="SCAN TIME" value={scanMs + ' ms'} />
        </div>

        <div style={{ border: '1px solid var(--rule)' }}>
          <div className="ff-mono upper" style={{
            display: 'grid', gridTemplateColumns: '60px 1fr 1fr 60px 80px 100px',
            gap: 12, padding: '10px 14px', borderBottom: '1px solid var(--rule)',
            background: 'var(--surface)', fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em',
          }}>
            <span>SCORE</span>
            <span>RECORD A</span>
            <span>RECORD B</span>
            <span style={{ textAlign: 'right' }}>Δ</span>
            <span>SIGNAL</span>
            <span style={{ textAlign: 'right' }}>ACTION</span>
          </div>
          {pairs.map((p, i) => (
            <PairRow key={i} pair={p} kind={kind} k={k} onInspect={onInspect} onDismiss={onDismiss} />
          ))}
        </div>
      </div>
    );
  }

  function PairRow({ pair, kind, k, onInspect, onDismiss }) {
    const sa = k.get(pair.a), sb = k.get(pair.b);
    const conf = Math.round(pair.score * 100);
    const tone = pair.score >= 0.92 ? '#a04432' : pair.score >= 0.85 ? '#c79538' : 'var(--ink-3)';
    // dominant signal
    const br = useMemo(() => {
      const F_ = window.FuzzyEngine;
      if (!F_) return null;
      return F_.scoreBreakdown(sa, sb);
    }, [sa, sb]);
    const dominant = br ? Object.entries(br).filter(([k_]) => k_ !== 'composite' && br[k_] != null).sort((a, b) => b[1] - a[1])[0]?.[0] : '?';
    const subA = subtitle(pair.a, kind);
    const subB = subtitle(pair.b, kind);
    return (
      <div style={{
        display: 'grid', gridTemplateColumns: '60px 1fr 1fr 60px 80px 100px',
        gap: 12, padding: '12px 14px', borderBottom: '1px solid var(--rule-soft)',
        alignItems: 'center', cursor: 'pointer',
      }}
      onClick={() => onInspect(pair)}
      onMouseEnter={(e) => e.currentTarget.style.background = 'var(--surface-2)'}
      onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
        <span className="ff-mono num" style={{ fontSize: 14, color: tone, fontWeight: 600 }}>{conf}</span>
        <div style={{ minWidth: 0 }}>
          <div style={{ fontSize: 13, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{sa}</div>
          {subA && <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{subA}</div>}
        </div>
        <div style={{ minWidth: 0 }}>
          <div style={{ fontSize: 13, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{sb}</div>
          {subB && <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{subB}</div>}
        </div>
        <span className="ff-mono num" style={{ fontSize: 11, color: 'var(--ink-3)', textAlign: 'right' }}>
          {sa.length === sb.length ? '0' : (sa.length > sb.length ? '+' : '−') + Math.abs(sa.length - sb.length)}
        </span>
        <span className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.08em' }}>{dominant}</span>
        <div style={{ display: 'flex', gap: 6, justifyContent: 'flex-end' }} onClick={(e) => e.stopPropagation()}>
          <button onClick={() => onDismiss(pair)} className="ff-mono upper" title="Not duplicates"
            style={{ fontSize: 9, padding: '4px 8px', background: 'transparent', border: '1px solid var(--rule)', color: 'var(--ink-3)', cursor: 'pointer', letterSpacing: '.08em' }}>
            ✕
          </button>
          <button onClick={() => onInspect(pair)} className="ff-mono upper"
            style={{ fontSize: 9, padding: '4px 10px', background: 'var(--ink)', color: 'var(--bg)', border: 0, cursor: 'pointer', letterSpacing: '.08em', fontWeight: 600 }}>
            REVIEW
          </button>
        </div>
      </div>
    );
  }

  function subtitle(item, kind) {
    if (kind === 'works') return item.iswc || (item.writers && item.writers[0]?.name) || '';
    if (kind === 'recordings') return item.isrc || item.primaryArtistName || item.artist || '';
    if (kind === 'releases') return item.upc || item.primaryArtistName || item.artist || '';
    if (kind === 'parties') return item.ipi || item.role || item.kind || '';
    return '';
  }

  // ── Inspect drawer ───────────────────────────────────────────────
  function InspectDrawer({ pair, kind, onClose, onMerge, onDismiss }) {
    const k = KINDS.find(x => x.v === kind);
    const sa = k.get(pair.a), sb = k.get(pair.b);
    const F_ = window.FuzzyEngine;
    const br = F_ ? F_.scoreBreakdown(sa, sb) : null;

    return (
      <>
        <div onClick={onClose} style={{
          position: 'fixed', inset: 0, background: 'rgba(11,11,11,.4)', zIndex: 100,
        }} />
        <div style={{
          position: 'fixed', top: 0, right: 0, bottom: 0, width: 'min(640px, 92vw)',
          background: 'var(--bg)', borderLeft: '1px solid var(--rule)', zIndex: 101,
          overflowY: 'auto', boxShadow: '-12px 0 32px rgba(0,0,0,.18)',
        }}>
          <div style={{ padding: 24, borderBottom: '1px solid var(--rule)' }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }}>
              <div>
                <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.12em' }}>REVIEW DUPLICATE PAIR</div>
                <div style={{ fontSize: 22, fontWeight: 400, marginTop: 4 }}>Composite score · <span className="ff-mono num" style={{ fontWeight: 600 }}>{Math.round(pair.score * 100)}</span></div>
              </div>
              <button onClick={onClose} className="ff-mono"
                style={{ background: 'transparent', border: '1px solid var(--rule)', padding: '4px 10px', color: 'var(--ink-2)', cursor: 'pointer' }}>✕ Close</button>
            </div>
          </div>

          {/* Side-by-side */}
          <div style={{ padding: 24, borderBottom: '1px solid var(--rule)' }}>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
              <RecordCard item={pair.a} kind={kind} k={k} side="A" onKeep={() => onMerge(pair, pair.a)} />
              <RecordCard item={pair.b} kind={kind} k={k} side="B" onKeep={() => onMerge(pair, pair.b)} />
            </div>
          </div>

          {/* Algorithm breakdown */}
          {br && (
            <div style={{ padding: 24, borderBottom: '1px solid var(--rule)' }}>
              <div className="ff-mono upper" style={{ fontSize: 10, color: 'var(--ink-3)', letterSpacing: '.12em', marginBottom: 12 }}>ALGORITHM BREAKDOWN</div>
              <div style={{ display: 'grid', gridTemplateColumns: '160px 1fr 50px', gap: 12, alignItems: 'center' }}>
                {ALG_ROWS.map(row => {
                  const v = br[row.k];
                  if (v == null) return (
                    <React.Fragment key={row.k}>
                      <div style={{ fontSize: 12, color: 'var(--ink-3)' }}>{row.l}</div>
                      <div style={{ fontSize: 10, color: 'var(--ink-4)', fontStyle: 'italic' }}>n/a · corpus DF unavailable</div>
                      <div />
                    </React.Fragment>
                  );
                  const pct = Math.round(v * 100);
                  return (
                    <React.Fragment key={row.k}>
                      <div style={{ fontSize: 12 }}>{row.l}</div>
                      <div style={{ height: 6, background: 'var(--surface-2)', position: 'relative' }}>
                        <div style={{ position: 'absolute', inset: 0, right: `${100 - pct}%`, background: pct >= 85 ? '#3a8a52' : pct >= 60 ? '#c79538' : 'var(--ink-3)' }} />
                      </div>
                      <span className="ff-mono num" style={{ fontSize: 11, textAlign: 'right' }}>{pct}</span>
                    </React.Fragment>
                  );
                })}
              </div>
            </div>
          )}

          {/* Actions */}
          <div style={{ padding: 24, display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
            <button onClick={() => onDismiss(pair)} className="ff-mono upper"
              style={{ fontSize: 11, letterSpacing: '.1em', padding: '8px 16px', background: 'transparent', color: 'var(--ink-2)', border: '1px solid var(--rule)', cursor: 'pointer' }}>
              Not duplicates
            </button>
            <button onClick={() => onMerge(pair, pair.a)} className="ff-mono upper"
              style={{ fontSize: 11, letterSpacing: '.1em', padding: '8px 16px', background: 'var(--ink)', color: 'var(--bg)', border: 0, cursor: 'pointer', fontWeight: 600 }}>
              Keep A · merge B into it
            </button>
            <button onClick={() => onMerge(pair, pair.b)} className="ff-mono upper"
              style={{ fontSize: 11, letterSpacing: '.1em', padding: '8px 16px', background: 'var(--ink)', color: 'var(--bg)', border: 0, cursor: 'pointer', fontWeight: 600 }}>
              Keep B · merge A into it
            </button>
          </div>
        </div>
      </>
    );
  }

  const ALG_ROWS = [
    { k: 'jaccard',       l: 'Token Jaccard' },
    { k: 'trigram',       l: 'Trigram cosine' },
    { k: 'metaphone',     l: 'Double Metaphone' },
    { k: 'damerau',       l: 'Damerau-Levenshtein' },
    { k: 'smithWaterman', l: 'Smith-Waterman' },
    { k: 'softTfidf',     l: 'Soft-TFIDF' },
  ];

  function RecordCard({ item, kind, k, side, onKeep }) {
    const sub = subtitle(item, kind);
    return (
      <div style={{ border: '1px solid var(--rule)', padding: 14 }}>
        <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.12em', marginBottom: 8 }}>
          RECORD · {side}
        </div>
        <div style={{ fontSize: 14, fontWeight: 500, lineHeight: 1.3, marginBottom: 6 }}>{k.get(item)}</div>
        {sub && <div className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-2)', marginBottom: 8 }}>{sub}</div>}
        <div className="ff-mono" style={{ fontSize: 9, color: 'var(--ink-4)', letterSpacing: '.04em' }}>id · {item.id || '—'}</div>
        {item.createdAt && <div className="ff-mono" style={{ fontSize: 9, color: 'var(--ink-4)' }}>created · {fmtDate(item.createdAt)}</div>}
      </div>
    );
  }

  function fmtDate(ts) {
    try { return new Date(ts).toISOString().slice(0, 10); }
    catch (_) { return String(ts).slice(0, 10); }
  }

  // ── 02 · Blocking ────────────────────────────────────────────────
  function BlockingExplainer() {
    const [demo, setDemo] = useState('Despacito');
    const F_ = window.FuzzyEngine;
    const keys = F_ ? [...F_.blockingKeys(demo)] : [];
    return (
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24 }}>
        <div>
          <div className="ff-mono upper" style={{ fontSize: 10, color: 'var(--ink-3)', letterSpacing: '.12em', marginBottom: 12 }}>HOW BLOCKING KEEPS THIS CHEAP</div>
          <p style={{ fontSize: 13, lineHeight: 1.6, color: 'var(--ink-2)' }}>
            A naive sweep of 50,000 records is 1.25 billion pairs — far too many for browser-side scoring. Instead we generate a small set of <em>blocking keys</em> per record and only score pairs that share a key.
          </p>
          <p style={{ fontSize: 13, lineHeight: 1.6, color: 'var(--ink-2)' }}>
            Each record gets up to three keys: a 4-char metaphone prefix, the leading 3 normalized characters, and the first 6 chars of its first ≥4-char token. Two truly-similar records share at least one with overwhelming probability; two unrelated records almost never do.
          </p>
          <p style={{ fontSize: 13, lineHeight: 1.6, color: 'var(--ink-2)' }}>
            A 50k catalog scan typically lands at ~2M scored pairs — under a second on commodity hardware.
          </p>
        </div>
        <div>
          <div className="ff-mono upper" style={{ fontSize: 10, color: 'var(--ink-3)', letterSpacing: '.12em', marginBottom: 12 }}>TRY IT</div>
          <input value={demo} onChange={e => setDemo(e.target.value)} className="ff-mono"
            style={{ width: '100%', padding: '8px 10px', background: 'var(--paper)', border: '1px solid var(--rule)', color: 'var(--ink)', fontSize: 13, marginBottom: 12 }} />
          <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em', marginBottom: 8 }}>BLOCKING KEYS</div>
          <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginBottom: 14 }}>
            {keys.map(k => (
              <span key={k} className="ff-mono" style={{ fontSize: 11, padding: '4px 8px', background: 'var(--surface-2)', border: '1px solid var(--rule)' }}>{k}</span>
            ))}
            {keys.length === 0 && <span style={{ fontSize: 11, color: 'var(--ink-3)', fontStyle: 'italic' }}>(empty)</span>}
          </div>
          <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em', marginBottom: 4 }}>METAPHONE</div>
          <div className="ff-mono" style={{ fontSize: 13, marginBottom: 8 }}>{F_ ? F_.metaphone(demo) || '(none)' : '—'}</div>
          <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em', marginBottom: 4 }}>NORMALIZED</div>
          <div className="ff-mono" style={{ fontSize: 13 }}>{F_ ? F_.normalize(demo) || '(empty)' : '—'}</div>
        </div>
      </div>
    );
  }

  // ── 03 · Merge log ───────────────────────────────────────────────
  function MergeLog({ merges, onUndo, go }) {
    if (!merges.length) {
      return (
        <div style={{ padding: '60px 0', textAlign: 'center' }}>
          <div className="ff-mono upper" style={{ fontSize: 10, color: 'var(--ink-3)', letterSpacing: '.12em', marginBottom: 8 }}>NO MERGES YET</div>
          <div style={{ fontSize: 13, color: 'var(--ink-3)' }}>Approved merges from the Candidates tab show up here, with one-click undo.</div>
        </div>
      );
    }
    return (
      <div style={{ border: '1px solid var(--rule)' }}>
        <div className="ff-mono upper" style={{
          display: 'grid', gridTemplateColumns: '120px 80px 1fr 1fr 60px 80px',
          gap: 12, padding: '10px 14px', borderBottom: '1px solid var(--rule)',
          background: 'var(--surface)', fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em',
        }}>
          <span>WHEN</span>
          <span>KIND</span>
          <span>KEPT</span>
          <span>MERGED IN</span>
          <span style={{ textAlign: 'right' }}>SCORE</span>
          <span style={{ textAlign: 'right' }}>UNDO</span>
        </div>
        {merges.map(m => (
          <div key={m.id} style={{
            display: 'grid', gridTemplateColumns: '120px 80px 1fr 1fr 60px 80px',
            gap: 12, padding: '10px 14px', borderBottom: '1px solid var(--rule-soft)', alignItems: 'center',
          }}>
            <span className="ff-mono" style={{ fontSize: 11, color: 'var(--ink-2)' }}>{fmtTs(m.ts)}</span>
            <span className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.08em' }}>{m.kind}</span>
            <span style={{ fontSize: 12, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.keepLabel}</span>
            <span style={{ fontSize: 12, color: 'var(--ink-2)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.dropLabel}</span>
            <span className="ff-mono num" style={{ fontSize: 11, textAlign: 'right' }}>{Math.round(m.score * 100)}</span>
            <button onClick={() => onUndo(m)} className="ff-mono upper"
              style={{ fontSize: 9, padding: '3px 8px', background: 'transparent', border: '1px solid var(--rule)', color: 'var(--ink-2)', cursor: 'pointer', letterSpacing: '.08em' }}>
              UNDO
            </button>
          </div>
        ))}
      </div>
    );
  }

  function fmtTs(ts) {
    try {
      const d = new Date(ts);
      return d.toISOString().slice(0, 10) + ' ' + d.toTimeString().slice(0, 5);
    } catch (_) { return ''; }
  }

  // ── KPI cell ─────────────────────────────────────────────────────
  function Kpi({ label, value, tone }) {
    const c = tone === 'warn' ? '#a04432' : tone === 'ok' ? '#3a8a52' : 'var(--ink)';
    return (
      <div style={{ padding: '14px 16px', borderRight: '1px solid var(--rule)' }}>
        <div className="ff-mono upper" style={{ fontSize: 9, color: 'var(--ink-3)', letterSpacing: '.1em', marginBottom: 4 }}>{label}</div>
        <div className="ff-mono num" style={{ fontSize: 20, fontWeight: 500, color: c }}>{value}</div>
      </div>
    );
  }

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