/* global React, WORKS, RECORDINGS, RELEASES, AGREEMENTS, Ic, Pill, Section */
// ───────────────────────────────────────────────────────────── ISSUES
// Top-level "Data Quality & Anomaly Inbox" — every flagged issue that needs human review,
// across the catalog. Designed against astro.anomaly_reports (entity_type, anomaly_type,
// severity, description, expected_value, actual_value, dsp_name, status, resolution).
//
// This is broader than royalties' statement-flagged callouts: it covers DSP discrepancies,
// CMO mismatches, missing metadata, ISWC/ISRC collisions, royalty-rate variances, and
// completeness issues across works/recordings/releases.

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

// ─────────────────────────── seeded RNG (deterministic mock)
function seed(s){let h=2166136261;for(let i=0;i<s.length;i++){h^=s.charCodeAt(i);h=Math.imul(h,16777619);}return ()=>((h=Math.imul(h^(h>>>13),3432918353))>>>0)/4294967296;}

// ─────────────────────────── anomaly taxonomy
// Severity tiers — drives sorting + colouring + SLA
const SEVERITY = {
  critical: { label:'CRITICAL', color:'#a04432', sla:1,  rank:0 },
  high:     { label:'HIGH',     color:'#c79538', sla:3,  rank:1 },
  medium:   { label:'MEDIUM',   color:'#7a6e2d', sla:7,  rank:2 },
  low:      { label:'LOW',      color:'#5a6a7a', sla:14, rank:3 },
};

// Anomaly types — what kind of issue, what entity, what's the playbook
const ANOMALY_TYPES = {
  // DSP / royalty discrepancies
  'rate_variance':       { label:'Rate variance',       cat:'royalty',  entity:'recording', desc:'DSP per-stream rate deviates from contract' },
  'unmatched_line':      { label:'Unmatched line',      cat:'royalty',  entity:'recording', desc:'Statement line cannot be linked to a known recording' },
  'duplicate_payment':   { label:'Duplicate payment',   cat:'royalty',  entity:'recording', desc:'Same period/source paid twice' },
  'fx_anomaly':          { label:'FX anomaly',          cat:'royalty',  entity:'recording', desc:'Exchange rate moved >3% during reporting period' },
  'missing_statement':   { label:'Missing statement',   cat:'royalty',  entity:'recording', desc:'Expected statement not received within SLA' },
  // Metadata / catalog quality
  'iswc_collision':      { label:'ISWC collision',      cat:'metadata', entity:'work',      desc:'Two works share the same ISWC' },
  'isrc_collision':      { label:'ISRC collision',      cat:'metadata', entity:'recording', desc:'Two recordings share the same ISRC' },
  'missing_iswc':        { label:'Missing ISWC',        cat:'metadata', entity:'work',      desc:'Work has no ISWC assigned' },
  'missing_isrc':        { label:'Missing ISRC',        cat:'metadata', entity:'recording', desc:'Recording has no ISRC assigned' },
  'incomplete_splits':   { label:'Splits ≠ 100%',       cat:'metadata', entity:'work',      desc:'Writer splits do not total 100%' },
  'orphan_recording':    { label:'Orphan recording',    cat:'metadata', entity:'recording', desc:'Recording has no linked work' },
  'duplicate_work':      { label:'Duplicate work',      cat:'metadata', entity:'work',      desc:'Two works appear to be the same composition' },
  // CWR / society
  'cwr_rejection':       { label:'CWR rejection',       cat:'cwr',      entity:'work',      desc:'Society returned an error on registration' },
  'cwr_no_ack':          { label:'CWR no ack',          cat:'cwr',      entity:'work',      desc:'Registration sent, no acknowledgement after 30d' },
  'society_mismatch':    { label:'Society mismatch',    cat:'cwr',      entity:'work',      desc:'Society holds different splits than our record' },
  // Distribution / DDEX
  'ddex_rejection':      { label:'DDEX rejection',      cat:'ddex',     entity:'release',   desc:'DSP rejected the delivery package' },
  'qc_fail':             { label:'QC fail',             cat:'ddex',     entity:'release',   desc:'Asset QC found issues (audio, art, metadata)' },
  // Rights / agreements
  'agreement_expiring':  { label:'Agreement expiring',  cat:'rights',   entity:'agreement', desc:'Agreement expires within 60 days' },
  'territory_overlap':   { label:'Territory overlap',   cat:'rights',   entity:'agreement', desc:'Two agreements claim same rights in same territory' },
  'claim_conflict':      { label:'Ownership conflict',  cat:'rights',   entity:'work',      desc:'Two parties claim overlapping shares on the same work' },
  // Black-box (unallocated society pools)
  'blackbox_urgent':     { label:'Black-box · urgent',  cat:'royalty',  entity:'work',      desc:'Society holds unallocated pool with distribution window closing' },
  'blackbox_open':       { label:'Black-box · open',    cat:'royalty',  entity:'work',      desc:'Society holds unallocated pool — claim window open' },
};

// Categories for filter chips
const CATEGORIES = [
  { k:'all',      label:'All' },
  { k:'royalty',  label:'Royalty / Black-box' },
  { k:'metadata', label:'Metadata' },
  { k:'cwr',      label:'CWR / Society' },
  { k:'ddex',     label:'DDEX' },
  { k:'rights',   label:'Rights / Conflicts' },
];

// ─────────────────────────── deterministic anomaly generation
const NOW = new Date('2026-04-30');
function daysAgo(n){ const d=new Date(NOW); d.setDate(d.getDate()-n); return d; }

// DSPs — display labels for issue sources. No ref table for DSPs; this is a
// curated subset of the major platforms most likely to surface ingestion issues.
const DSPS = ['Spotify','Apple Music','YouTube Content ID','Amazon Music','TikTok','Deezer','Tidal','Pandora'];
// Societies — sourced from the central Societies directory (CWR-eligible subset).
// Falls back to a hand-picked list if REF hasn't loaded yet.
const SOCIETIES = (window.SOC_CWR || ['ASCAP','BMI','PRS for Music','GEMA','SACEM','JASRAC','SOCAN','SIAE']).slice(0, 12);
const REPORTERS = ['parser v3.4.1','matcher v2.1','m.lee','r.peters','a.cohen','p.tibbets','system','j.alvarez','quality.bot'];
const STATUSES = ['open','investigating','open','open','open','resolved','dismissed','open','investigating'];

function buildAnomalies() {
  if (window.__ANOMALIES_CACHE) return window.__ANOMALIES_CACHE;
  const r = seed('astro·anomalies·v1');
  const works = (typeof window.WORKS !== 'undefined') ? window.WORKS : [];
  const recs  = (typeof window.RECORDINGS !== 'undefined') ? window.RECORDINGS : [];
  const rels  = (typeof window.RELEASES !== 'undefined') ? window.RELEASES : [];
  const ags   = (typeof window.AGREEMENTS !== 'undefined') ? window.AGREEMENTS : [];

  const types = Object.keys(ANOMALY_TYPES);
  const sevs  = ['critical','high','high','medium','medium','medium','low','low','low'];
  const out = [];

  // Generate ~140 anomalies, weighted by realistic frequency
  for (let i = 0; i < 140; i++) {
    const tk = types[Math.floor(r()*types.length)];
    const t = ANOMALY_TYPES[tk];
    const sev = sevs[Math.floor(r()*sevs.length)];
    const status = STATUSES[Math.floor(r()*STATUSES.length)];
    const ageDays = Math.floor(r()*45);

    // Pick a target entity
    let entity = null;
    let isrc = null, upc = null, iswc = null;
    if (t.entity === 'work' && works.length) {
      entity = works[Math.floor(r()*works.length)];
      iswc = entity.iswc || null;
    } else if (t.entity === 'recording' && recs.length) {
      entity = recs[Math.floor(r()*recs.length)];
      isrc = entity.isrc || null;
    } else if (t.entity === 'release' && rels.length) {
      entity = rels[Math.floor(r()*rels.length)];
      upc = entity.upc || null;
    } else if (t.entity === 'agreement' && ags.length) {
      entity = ags[Math.floor(r()*ags.length)];
    }
    if (!entity) continue;

    // Build expected/actual values per type
    let expected = '', actual = '', description = t.desc;
    if (tk === 'rate_variance') {
      const e = (0.0040 + r()*0.0010).toFixed(5);
      const a = (parseFloat(e) * (0.55 + r()*0.30)).toFixed(5);
      expected = `$${e}/stream`;
      actual = `$${a}/stream`;
      description = `Spotify per-stream rate ${(((parseFloat(a)-parseFloat(e))/parseFloat(e))*100).toFixed(1)}% below contract floor`;
    } else if (tk === 'unmatched_line') {
      expected = '1 line matched';
      actual = '0 lines matched';
      description = `Statement line "${(entity.title||'untitled').toUpperCase()}" — no fingerprint match in catalog`;
    } else if (tk === 'duplicate_payment') {
      const amt = (200 + r()*1800).toFixed(2);
      expected = `$${amt} (1×)`;
      actual = `$${(amt*2).toFixed(2)} (2×)`;
      description = `Q4 2025 royalty paid twice — same statement ID, two ingestions`;
    } else if (tk === 'fx_anomaly') {
      expected = '1 EUR = 1.085 USD';
      actual = '1 EUR = 1.142 USD';
      description = `EUR/USD moved 5.2% during reporting period — affects ${SOCIETIES[Math.floor(r()*4)+2]} statement`;
    } else if (tk === 'missing_statement') {
      expected = 'Q1 2026 by 2026-04-15';
      actual = 'not received';
      description = `${DSPS[Math.floor(r()*DSPS.length)]} Q1 2026 statement is ${ageDays}d overdue`;
    } else if (tk === 'iswc_collision') {
      expected = '1 work per ISWC';
      actual = '2 works share ISWC';
      description = `ISWC ${iswc||'T-300.123.456-7'} also assigned to "${(works[Math.floor(r()*works.length)]||{title:'unknown'}).title}"`;
    } else if (tk === 'isrc_collision') {
      expected = '1 recording per ISRC';
      actual = '2 recordings share ISRC';
      description = `ISRC ${isrc||'USRC12345678'} also on a different master`;
    } else if (tk === 'missing_iswc') {
      expected = 'ISWC required for CWR';
      actual = 'no ISWC';
      description = `Cannot register with PROs until ISWC is assigned`;
    } else if (tk === 'missing_isrc') {
      expected = 'ISRC required for distribution';
      actual = 'no ISRC';
      description = `Cannot deliver to DSPs without ISRC`;
    } else if (tk === 'incomplete_splits') {
      const total = (85 + r()*20).toFixed(2);
      expected = '100.00%';
      actual = `${total}%`;
      description = `Writer splits sum to ${total}% — ${parseFloat(total) > 100 ? 'over-allocated' : 'unallocated remainder'}`;
    } else if (tk === 'orphan_recording') {
      expected = 'linked to a work';
      actual = 'no linked work';
      description = `Recording has no parent composition — cannot collect publishing royalties`;
    } else if (tk === 'duplicate_work') {
      const other = works[Math.floor(r()*works.length)] || {title:'Unknown'};
      expected = '1 canonical work';
      actual = '2 candidate works';
      description = `Title + writer match with "${other.title}" — possible duplicate`;
    } else if (tk === 'cwr_rejection') {
      const codes = ['DR-009','TR-002','SW-005','PW-015','GA-007'];
      expected = 'NWR_VALID';
      actual = codes[Math.floor(r()*codes.length)];
      description = `${SOCIETIES[Math.floor(r()*SOCIETIES.length)]} rejected NWR — ${actual === 'DR-009' ? 'duplicate registration' : actual === 'TR-002' ? 'invalid title' : 'split mismatch'}`;
    } else if (tk === 'cwr_no_ack') {
      expected = 'ACK within 30d';
      actual = `${30+ageDays}d no ack`;
      description = `${SOCIETIES[Math.floor(r()*SOCIETIES.length)]} has not acknowledged registration submitted ${30+ageDays}d ago`;
    } else if (tk === 'society_mismatch') {
      expected = 'matches our shares';
      actual = 'shares differ';
      description = `${SOCIETIES[Math.floor(r()*SOCIETIES.length)]} holds ${(40+r()*20).toFixed(1)}% — we have ${(50+r()*15).toFixed(1)}% on file`;
    } else if (tk === 'ddex_rejection') {
      expected = 'DDEX_VALID';
      actual = 'XSD_FAIL';
      description = `${DSPS[Math.floor(r()*4)]} rejected ERN 4.3 delivery — schema validation failed on ResourceList`;
    } else if (tk === 'qc_fail') {
      const issues = ['LUFS exceeds -14 spec','Cover art below 3000×3000','Explicit flag missing','Duration mismatch','ISRC checksum invalid'];
      expected = 'QC_PASS';
      actual = 'QC_FAIL';
      description = issues[Math.floor(r()*issues.length)];
    } else if (tk === 'agreement_expiring') {
      const d = Math.floor(r()*60);
      expected = `>60d remaining`;
      actual = `${d}d remaining`;
      description = `Agreement expires in ${d} days — renewal not yet started`;
    } else if (tk === 'territory_overlap') {
      const t1 = ['US','GB','DE','FR','JP'][Math.floor(r()*5)];
      const t2 = ['World','Worldwide ex-US','EU'][Math.floor(r()*3)];
      expected = '1 grant per territory';
      actual = `${t1} ⊆ ${t2}`;
      description = `${t1} rights granted in two overlapping agreements`;
    }

    const dspName = (t.cat === 'royalty' || t.cat === 'ddex') ? DSPS[Math.floor(r()*DSPS.length)] : (t.cat === 'cwr' ? SOCIETIES[Math.floor(r()*SOCIETIES.length)] : null);

    out.push({
      id: `anom_${String(i+1).padStart(4,'0')}`,
      report_id: `RPT-${(2026000+i).toString()}`,
      anomaly_type: tk,
      anomaly_meta: t,
      severity: sev,
      status,
      description,
      expected_value: expected,
      actual_value: actual,
      entity_type: t.entity,
      entity_id: entity.id,
      entity_label: entity.title || entity.name || entity.id,
      entity_secondary: entity.artist || entity.writer || entity.label || (entity.parties && entity.parties[0]) || '',
      isrc, upc, iswc,
      dsp_name: dspName,
      reported_by: REPORTERS[Math.floor(r()*REPORTERS.length)],
      reported_to: r() < 0.5 ? null : ['compliance','royalty-ops','catalog-mgmt','rights-team'][Math.floor(r()*4)],
      created_at: daysAgo(ageDays),
      age_days: ageDays,
      resolution: status === 'resolved' ? ['Adjusted', 'Re-matched manually','Re-delivered','Marked as duplicate-OK','Auto-corrected'][Math.floor(r()*5)] : null,
      resolved_at: status === 'resolved' ? daysAgo(Math.max(0, ageDays - Math.floor(r()*10) - 1)) : null,
    });
  }
  return out;
}

// Pull real claim conflicts from window.CLAIMS (open ones) into the anomaly inbox.
// This means the Issues queue includes ownership disputes naturally instead of
// living on a separate page.
function buildClaimAnomalies() {
  const claims = (typeof window.CLAIMS !== 'undefined' ? window.CLAIMS : []);
  const out = [];
  claims.forEach((c, i) => {
    if (c.status !== 'open') return;
    const sev = c.severity === 'high' ? 'critical' : c.severity === 'mid' ? 'high' : 'medium';
    out.push({
      id: `anom_claim_${c.id}`,
      report_id: c.id,
      anomaly_type: 'claim_conflict',
      anomaly_meta: ANOMALY_TYPES['claim_conflict'],
      severity: sev,
      status: 'open',
      description: `${c.claimant} (${c.pct}%) vs. ${c.party} (${100-c.pct}%) — ${c.method.toUpperCase()} claim on "${c.work}"`,
      expected_value: '1 canonical ownership',
      actual_value: `${c.claimant} ↔ ${c.party}`,
      entity_type: 'work',
      entity_id: c.iswc || c.id,
      entity_label: c.work,
      entity_secondary: c.iswc || '',
      iswc: c.iswc || null,
      isrc: null, upc: null,
      dsp_name: null,
      reported_by: c.method === 'cwr' ? 'system' : c.claimant.toLowerCase().replace(/[^a-z]/g,''),
      reported_to: 'rights-team',
      created_at: daysAgo(c.age),
      age_days: c.age,
      resolution: null, resolved_at: null,
      // breadcrumb for the drawer to deep-link back into the Claims experience
      _claim_id: c.id,
    });
  });
  return out;
}

// Pull urgent black-box pools into the inbox as a single anomaly per pool.
// Reads window.__BLACKBOX_POOLS if exposed; falls back to a small synthetic set
// driven by SOCIETIES so the rows still appear when workspace-screens.jsx hasn't
// stashed pools yet.
function buildBlackBoxAnomalies() {
  const pools = window.__BLACKBOX_POOLS || [];
  const out = [];
  pools.forEach((p, i) => {
    if (p.status === 'forfeited') return;
    const urgent = p.status === 'urgent';
    const sev = urgent ? 'critical' : 'medium';
    const days = p.daysToDistribution;
    out.push({
      id: `anom_bb_${p.id}`,
      report_id: p.id,
      anomaly_type: urgent ? 'blackbox_urgent' : 'blackbox_open',
      anomaly_meta: ANOMALY_TYPES[urgent ? 'blackbox_urgent' : 'blackbox_open'],
      severity: sev,
      status: 'open',
      description: `${p.societyName || p.society} holds $${(p.totalUsd/1000).toFixed(0)}K unallocated for ${p.period} — ${days < 0 ? 'window closed' : days < 30 ? `${days}d to claim` : `${days}d to distribution`}`,
      expected_value: 'allocated to rightsholders',
      actual_value: `${p.lineCount} lines unmatched`,
      entity_type: 'work',
      entity_id: p.id,
      entity_label: `${p.society} ${p.period} pool`,
      entity_secondary: `${p.lineCount} candidate works · ${p.currency}`,
      iswc: null, isrc: null, upc: null,
      dsp_name: p.society,
      reported_by: 'system',
      reported_to: 'royalty-ops',
      created_at: daysAgo(Math.max(0, 30 - Math.min(30, days))),
      age_days: Math.max(0, 30 - Math.min(30, days)),
      resolution: null, resolved_at: null,
      _pool_id: p.id,
    });
  });
  return out;
}
// Eagerly build once and stash so the sidebar badge can read live counts before <ScreenIssues> mounts.
window.__ANOMALIES_CACHE = buildAnomalies();
// Defer claims + blackbox merge until other modules have published their data.
// Issues screen reads __ALL_ANOMALIES; if it's missing or stale, we recompute.
function buildAllAnomalies() {
  const base    = window.__ANOMALIES_CACHE || buildAnomalies();
  const claims  = buildClaimAnomalies();
  const bb      = buildBlackBoxAnomalies();
  return [...base, ...claims, ...bb];
}
window.__buildAllAnomalies = buildAllAnomalies;
// Recompute the open count to include claims + black-box so the rail badge tells the truth
window.__ISSUES_OPEN_COUNT = buildAllAnomalies().filter(a => a.status==='open' || a.status==='investigating').length;

// ─────────────────────────── small components
function Stat({ label, value, sub, accent, last }) {
  return (
    <div style={{padding:'18px 22px',borderRight: last ? 'none' : '1px solid var(--rule)',flex:1,minWidth:0}}>
      <div className="ff-mono upper" style={{fontSize:9,color:'var(--ink-3)',letterSpacing:'.1em',marginBottom:8}}>{label}</div>
      <div className="ff-display num" style={{fontSize:36,fontWeight:600,letterSpacing:'-0.04em',lineHeight:1, color: accent || 'var(--ink)'}}>{value}</div>
      {sub && <div className="ff-mono" style={{fontSize:10,color:'var(--ink-3)',marginTop:6,letterSpacing:'.02em'}}>{sub}</div>}
    </div>
  );
}

function SeverityChip({ s }) {
  const m = SEVERITY[s];
  return (
    <span className="ff-mono upper" style={{fontSize:9,letterSpacing:'.12em',padding:'3px 7px',
      border:`1px solid ${m.color}`, color:m.color, fontWeight:600, display:'inline-flex',alignItems:'center',gap:5}}>
      <span style={{width:5,height:5,background:m.color}}/>
      {m.label}
    </span>
  );
}

function StatusChip({ s }) {
  const tones = {
    open:          { fg:'var(--ink)',  bd:'var(--ink-2)' },
    investigating: { fg:'#1a5d8c',     bd:'#1a5d8c' },
    resolved:      { fg:'#2d6a3a',     bd:'#2d6a3a' },
    dismissed:     { fg:'var(--ink-3)',bd:'var(--ink-3)' },
  };
  const t = tones[s] || tones.open;
  return (
    <span className="ff-mono upper" style={{fontSize:9,letterSpacing:'.1em',padding:'2px 6px',
      border:`1px solid ${t.bd}`, color:t.fg}}>{s}</span>
  );
}

function CategoryDot({ cat }) {
  const c = {
    royalty:'#1a5d8c', metadata:'#7a5a8c', cwr:'#c79538', ddex:'#2d6a3a', rights:'#a04432'
  }[cat] || 'var(--ink-3)';
  return <span style={{width:6,height:6,background:c,display:'inline-block',flexShrink:0}}/>;
}

// ─────────────────────────── top distribution chart
// Stacked horizontal bar showing severity breakdown across each category
function CategoryDistribution({ items }) {
  const cats = ['royalty','metadata','cwr','ddex','rights'];
  const open = items.filter(a => a.status === 'open' || a.status === 'investigating');
  const byCatSev = {};
  cats.forEach(c => byCatSev[c] = { critical:0, high:0, medium:0, low:0, total:0 });
  open.forEach(a => {
    const cat = a.anomaly_meta.cat;
    if (byCatSev[cat]) {
      byCatSev[cat][a.severity]++;
      byCatSev[cat].total++;
    }
  });
  const max = Math.max(1, ...cats.map(c => byCatSev[c].total));

  return (
    <div style={{border:'1px solid var(--rule)',marginBottom:24}}>
      <div style={{padding:'8px 14px',borderBottom:'1px solid var(--rule-soft)',background:'var(--bg-2)',
        display:'flex',justifyContent:'space-between',alignItems:'center'}}>
        <span className="ff-mono upper" style={{fontSize:10,letterSpacing:'.1em'}}>OPEN ISSUES BY CATEGORY · SEVERITY MIX</span>
        <div style={{display:'flex',gap:14,fontSize:10,alignItems:'center'}}>
          {['critical','high','medium','low'].map(s => (
            <div key={s} style={{display:'flex',gap:5,alignItems:'center'}}>
              <span style={{width:8,height:8,background:SEVERITY[s].color}}/>
              <span className="ff-mono upper" style={{letterSpacing:'.08em',color:'var(--ink-3)'}}>{SEVERITY[s].label}</span>
            </div>
          ))}
        </div>
      </div>
      <div style={{padding:'12px 16px',display:'flex',flexDirection:'column',gap:6}}>
        {cats.map(c => {
          const row = byCatSev[c];
          return (
            <div key={c} style={{display:'grid',gridTemplateColumns:'92px 1fr 40px',gap:10,alignItems:'center'}}>
              <div style={{display:'flex',gap:8,alignItems:'center'}}>
                <CategoryDot cat={c}/>
                <span className="ff-mono upper" style={{fontSize:10,letterSpacing:'.08em',color:'var(--ink-2)'}}>{c}</span>
              </div>
              <div style={{height:12,background:'var(--bg-2)',position:'relative',display:'flex'}}>
                {row.total === 0 ? null : (
                  ['critical','high','medium','low'].map(s => {
                    if (!row[s]) return null;
                    const segW = (row[s] / max) * 100;
                    return <div key={s} title={`${row[s]} ${s}`} style={{width:`${segW}%`,height:'100%',background:SEVERITY[s].color,borderRight:'1px solid var(--bg)'}}/>;
                  })
                )}
              </div>
              <div className="ff-mono num" style={{fontSize:12,textAlign:'right',color:row.total ? 'var(--ink)' : 'var(--ink-3)',fontWeight:600}}>{row.total || '—'}</div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ─────────────────────────── filter bar
function FilterBar({ filters, setFilters, types, counts }) {
  return (
    <div style={{border:'1px solid var(--rule)',marginBottom:24,background:'var(--bg)'}}>
      {/* Row 1: status + category */}
      <div style={{display:'flex',borderBottom:'1px solid var(--rule-soft)',alignItems:'stretch'}}>
        <div style={{padding:'10px 16px',borderRight:'1px solid var(--rule-soft)',display:'flex',alignItems:'center',gap:8}}>
          <span className="ff-mono upper" style={{fontSize:9,letterSpacing:'.1em',color:'var(--ink-3)'}}>STATUS</span>
          {['all','open','investigating','resolved','dismissed'].map(s => (
            <button key={s} onClick={()=>setFilters(f=>({...f,status:s}))}
              className="ff-mono upper"
              style={{fontSize:10,letterSpacing:'.08em',padding:'4px 8px',
                background: filters.status === s ? 'var(--ink)' : 'transparent',
                color: filters.status === s ? 'var(--bg)' : 'var(--ink)',
                border:'1px solid var(--rule)', cursor:'pointer'}}>
              {s} {s !== 'all' && counts.byStatus[s] ? `· ${counts.byStatus[s]}` : ''}
            </button>
          ))}
        </div>
        <div style={{padding:'10px 16px',display:'flex',alignItems:'center',gap:8,flex:1}}>
          <span className="ff-mono upper" style={{fontSize:9,letterSpacing:'.1em',color:'var(--ink-3)'}}>CAT</span>
          {CATEGORIES.map(c => (
            <button key={c.k} onClick={()=>setFilters(f=>({...f,category:c.k}))}
              className="ff-mono upper"
              style={{fontSize:10,letterSpacing:'.08em',padding:'4px 8px',
                background: filters.category === c.k ? 'var(--ink)' : 'transparent',
                color: filters.category === c.k ? 'var(--bg)' : 'var(--ink)',
                border:'1px solid var(--rule)', cursor:'pointer'}}>
              {c.label}
            </button>
          ))}
        </div>
      </div>
      {/* Row 2: severity + search */}
      <div style={{display:'flex',alignItems:'stretch'}}>
        <div style={{padding:'10px 16px',borderRight:'1px solid var(--rule-soft)',display:'flex',alignItems:'center',gap:8}}>
          <span className="ff-mono upper" style={{fontSize:9,letterSpacing:'.1em',color:'var(--ink-3)'}}>SEVERITY</span>
          {['all','critical','high','medium','low'].map(s => (
            <button key={s} onClick={()=>setFilters(f=>({...f,severity:s}))}
              className="ff-mono upper"
              style={{fontSize:10,letterSpacing:'.08em',padding:'4px 8px',
                background: filters.severity === s ? 'var(--ink)' : 'transparent',
                color: filters.severity === s ? 'var(--bg)' : (s !== 'all' ? SEVERITY[s].color : 'var(--ink)'),
                border:`1px solid ${filters.severity === s ? 'var(--ink)' : (s!=='all' ? SEVERITY[s].color : 'var(--rule)')}`,
                cursor:'pointer'}}>
              {s}
            </button>
          ))}
        </div>
        <div style={{padding:'8px 16px',display:'flex',alignItems:'center',gap:10,flex:1}}>
          <span className="ff-mono upper" style={{fontSize:9,letterSpacing:'.1em',color:'var(--ink-3)'}}>SEARCH</span>
          <input
            value={filters.q}
            onChange={(e)=>setFilters(f=>({...f,q:e.target.value}))}
            placeholder="title, ISRC, ISWC, DSP, anomaly type…"
            style={{flex:1,padding:'4px 6px',border:'1px solid var(--rule)',background:'var(--bg)',color:'var(--ink)',fontSize:12,fontFamily:'inherit'}}/>
          <span className="ff-mono num" style={{fontSize:11,color:'var(--ink-3)'}}>
            {counts.total.toLocaleString()} matches
          </span>
        </div>
      </div>
    </div>
  );
}

// ─────────────────────────── issue row
function IssueRow({ a, onOpen, isLast, hideStatus }) {
  const m = ANOMALY_TYPES[a.anomaly_type];
  const sla = SEVERITY[a.severity].sla;
  const closed = a.status === 'resolved' || a.status === 'dismissed';
  const overSla = !closed && a.age_days > sla;
  // SLA-consumed % — capped at 200% for the bar; >100% is the breach band
  const slaPct = closed ? 100 : Math.min(200, (a.age_days / sla) * 100);
  const slaTone = closed ? 'var(--ink-3)' : (slaPct >= 100 ? '#a04432' : (slaPct >= 80 ? '#c79538' : '#5a8a5a'));

  const cols = hideStatus
    ? '80px 110px 1.6fr 1.4fr 120px 70px'
    : '80px 110px 1.6fr 1.4fr 120px 90px 70px';

  return (
    <div onClick={onOpen}
      style={{display:'grid',gridTemplateColumns:cols,gap:0,
        padding:'12px 14px',borderBottom: isLast ? 'none' : '1px solid var(--rule-soft)',
        alignItems:'center',cursor:'pointer',
        background: overSla ? 'rgba(160,68,50,0.04)' : 'transparent'}}
      onMouseEnter={(e)=>{e.currentTarget.style.background = overSla ? 'rgba(160,68,50,0.08)' : 'var(--bg-2)'}}
      onMouseLeave={(e)=>{e.currentTarget.style.background = overSla ? 'rgba(160,68,50,0.04)' : 'transparent'}}
    >
      <SeverityChip s={a.severity}/>
      <div style={{display:'flex',gap:6,alignItems:'center'}}>
        <CategoryDot cat={m.cat}/>
        <span className="ff-mono upper" style={{fontSize:10,letterSpacing:'.06em'}}>{m.label}</span>
      </div>
      <div style={{minWidth:0,paddingRight:16}}>
        <div style={{fontSize:13,fontWeight:500,letterSpacing:'-0.005em',whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>{a.description}</div>
        <div className="ff-mono" style={{fontSize:10,color:'var(--ink-3)',marginTop:2,letterSpacing:'.02em'}}>
          {a.report_id}{a.dsp_name ? ` · ${a.dsp_name}` : ''}
        </div>
      </div>
      <div style={{minWidth:0,paddingRight:16}}>
        <div style={{fontSize:12,fontWeight:500,whiteSpace:'nowrap',overflow:'hidden',textOverflow:'ellipsis'}}>
          {a.entity_label}
        </div>
        <div className="ff-mono" style={{fontSize:10,color:'var(--ink-3)',marginTop:2,letterSpacing:'.04em'}}>
          {a.entity_type.toUpperCase()}{a.isrc ? ` · ${a.isrc}` : ''}{a.iswc ? ` · ${a.iswc}` : ''}{a.upc ? ` · ${a.upc}` : ''}
        </div>
      </div>
      {/* AGE / SLA — text on top, consumed-bar below; bar shows green→amber→red gradient */}
      <div style={{paddingRight:12}}>
        <div className="ff-mono" style={{fontSize:11,color: closed ? 'var(--ink-3)' : (overSla ? '#a04432' : 'var(--ink-2)'),letterSpacing:'.02em',display:'flex',justifyContent:'space-between',alignItems:'baseline',gap:6}}>
          <span>{a.age_days === 0 ? 'today' : `${a.age_days}d`}</span>
          <span style={{fontSize:9,color:'var(--ink-3)',letterSpacing:'.04em'}}>/ {sla}d</span>
        </div>
        <div title={`${Math.round(slaPct)}% of SLA${overSla ? ' — BREACHED':''}`}
          style={{height:3,background:'var(--rule-soft)',marginTop:5,position:'relative',overflow:'hidden'}}>
          <div style={{height:'100%',width:`${Math.min(100, slaPct)}%`,background:slaTone,transition:'width .2s'}}/>
          {slaPct > 100 && (
            <div style={{position:'absolute',right:0,top:0,height:'100%',width:`${Math.min(50, slaPct-100)/2}%`,
              background:'repeating-linear-gradient(135deg,#a04432 0 3px,#7a3024 3px 6px)'}}/>
          )}
        </div>
      </div>
      {!hideStatus && <StatusChip s={a.status}/>}
      <div className="ff-mono upper" style={{fontSize:10,color:'var(--ink-3)',letterSpacing:'.08em',textAlign:'right'}}>
        {a._promoted_task_id && (
          <div title="In your Inbox" style={{display:'inline-flex',alignItems:'center',gap:4,marginRight:8,color:'var(--ink-2)'}}>
            <span style={{display:'inline-block',width:5,height:5,borderRadius:'50%',background:'var(--ink-2)'}}/>
            inbox
          </div>
        )}
        open →
      </div>
    </div>
  );
}

// ─────────────────────────── issue drawer (right sheet)
function IssueDrawer({ a, onClose, go }) {
  if (!a) return null;
  const [, force] = React.useReducer(x => x + 1, 0);
  const m = ANOMALY_TYPES[a.anomaly_type];
  const sla = SEVERITY[a.severity].sla;
  const overSla = a.status !== 'resolved' && a.status !== 'dismissed' && a.age_days > sla;

  // Suggested actions per anomaly type
  const actions = {
    'rate_variance':       ['Re-fetch DSP rate card', 'Open dispute with DSP', 'Adjust expected rate'],
    'unmatched_line':      ['Run fuzzy matcher again', 'Manually link to recording', 'Mark as out-of-catalog'],
    'duplicate_payment':   ['Reverse duplicate', 'Confirm both legitimate', 'Investigate ingestion'],
    'fx_anomaly':          ['Re-base at period close FX', 'Use period-avg rate', 'Open with treasury'],
    'missing_statement':   ['Send reminder to source', 'Escalate to ops', 'Mark as expected delay'],
    'iswc_collision':      ['Merge works', 'Re-assign ISWC to one', 'Confirm both legitimate'],
    'isrc_collision':      ['Merge recordings', 'Re-assign ISRC', 'Confirm regional reissue'],
    'missing_iswc':        ['Request ISWC from PRO', 'Mark as not-eligible', 'Backfill from MusicBrainz'],
    'missing_isrc':        ['Generate ISRC', 'Pull from distributor', 'Mark as legacy-no-id'],
    'incomplete_splits':   ['Open work editor', 'Request missing share', 'Apply default to admin'],
    'orphan_recording':    ['Link to existing work', 'Create work from recording', 'Mark as covers/derivative'],
    'duplicate_work':      ['Compare side-by-side', 'Merge', 'Confirm distinct'],
    'cwr_rejection':       ['View rejection details', 'Fix and re-submit', 'Open with society'],
    'cwr_no_ack':          ['Re-send registration', 'Contact society', 'Mark as pending'],
    'society_mismatch':    ['Reconcile shares', 'Send NWR correction', 'Accept society version'],
    'ddex_rejection':      ['View DSP error log', 'Fix and re-deliver', 'Open with DSP'],
    'qc_fail':             ['View QC report', 'Re-master and re-deliver', 'Override QC (manual)'],
    'agreement_expiring':  ['Open renewal flow', 'Notify counterparty', 'Mark as not-renewing'],
    'territory_overlap':   ['Open both agreements', 'Amend conflicting clause', 'Escalate to legal'],
    'claim_conflict':      ['Open claim file', 'Request additional documentation', 'Escalate to legal'],
    'blackbox_urgent':     ['Auto-claim high-confidence lines', 'File ISWC with society', 'Manual review queue'],
    'blackbox_open':       ['Auto-claim high-confidence lines', 'File ISWC with society', 'Defer to next cycle'],
  }[a.anomaly_type] || ['Investigate', 'Resolve', 'Dismiss'];

  // Navigate to entity
  const openEntity = () => {
    onClose();
    // Claim conflict: take user to the work in question — the claims context
    // travels with the work record (overlay shows in-progress disputes).
    if (a._claim_id) {
      if (a.iswc) {
        // try to find matching work by ISWC; otherwise just go to claims-aware work surface
        const works = window.WORKS || [];
        const w = works.find(x => x.iswc === a.iswc);
        if (w) return go('work', { id: w.id });
      }
      window.toast && window.toast(`Opening ${a._claim_id} in claims context`, 'soft');
      return;
    }
    // Black-box: open the source society's pool (currently a toast — claim flow is multi-step)
    if (a._pool_id) {
      window.toast && window.toast(`Pool ${a._pool_id} — claim flow opens here`, 'soft');
      return;
    }
    if (a.entity_type === 'work') go('work', { id: a.entity_id });
    else if (a.entity_type === 'recording') window.dispatchEvent(new CustomEvent('astro-open-recording',{detail:{id:a.entity_id}}));
    else if (a.entity_type === 'release') window.dispatchEvent(new CustomEvent('astro-open-entity',{detail:{kind:'release',id:a.entity_id}}));
    else if (a.entity_type === 'agreement') window.dispatchEvent(new CustomEvent('astro-open-entity',{detail:{kind:'agreement',id:a.entity_id}}));
  };

  return (
    <>
      <div onClick={onClose} style={{position:'fixed',inset:0,background:'rgba(0,0,0,0.4)',zIndex:60}}/>
      <aside style={{position:'fixed',top:0,right:0,bottom:0,width:'min(640px,92vw)',background:'var(--bg)',
        borderLeft:'1px solid var(--rule)',zIndex:61,overflowY:'auto',display:'flex',flexDirection:'column'}}>

        {/* header */}
        <div style={{padding:'18px 24px',borderBottom:'1px solid var(--rule)',display:'flex',justifyContent:'space-between',alignItems:'flex-start',gap:16}}>
          <div style={{minWidth:0,flex:1}}>
            <div style={{display:'flex',gap:10,alignItems:'center',marginBottom:8}}>
              <SeverityChip s={a.severity}/>
              <StatusChip s={a.status}/>
              {overSla && <span className="ff-mono upper" style={{fontSize:9,letterSpacing:'.1em',color:'#a04432',padding:'2px 6px',border:'1px solid #a04432'}}>SLA BREACH</span>}
            </div>
            <div className="ff-display" style={{fontSize:22,fontWeight:600,letterSpacing:'-0.02em',lineHeight:1.2}}>
              {m.label}
            </div>
            <div className="ff-mono" style={{fontSize:11,color:'var(--ink-3)',marginTop:6,letterSpacing:'.02em'}}>
              {a.report_id} · created {a.age_days === 0 ? 'today' : `${a.age_days}d ago`} · by {a.reported_by}
            </div>
          </div>
          <button onClick={onClose} style={{background:'none',border:0,cursor:'pointer',padding:8,color:'var(--ink-3)'}}>
            <Ic.X width={20} height={20}/>
          </button>
        </div>

        {/* description */}
        <div style={{padding:'20px 24px',borderBottom:'1px solid var(--rule-soft)'}}>
          <div className="ff-mono upper" style={{fontSize:9,letterSpacing:'.1em',color:'var(--ink-3)',marginBottom:8}}>WHAT WE OBSERVED</div>
          <div style={{fontSize:14,lineHeight:1.55,letterSpacing:'-0.005em'}}>{a.description}</div>
        </div>

        {/* expected vs actual */}
        <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',borderBottom:'1px solid var(--rule-soft)'}}>
          <div style={{padding:'18px 24px',borderRight:'1px solid var(--rule-soft)'}}>
            <div className="ff-mono upper" style={{fontSize:9,letterSpacing:'.1em',color:'var(--ink-3)',marginBottom:8}}>EXPECTED</div>
            <div className="ff-mono" style={{fontSize:14,fontWeight:500}}>{a.expected_value || '—'}</div>
          </div>
          <div style={{padding:'18px 24px',background:'rgba(160,68,50,0.04)'}}>
            <div className="ff-mono upper" style={{fontSize:9,letterSpacing:'.1em',color:'var(--ink-3)',marginBottom:8}}>ACTUAL</div>
            <div className="ff-mono" style={{fontSize:14,fontWeight:500,color:'#a04432'}}>{a.actual_value || '—'}</div>
          </div>
        </div>

        {/* affected entity */}
        <div style={{padding:'20px 24px',borderBottom:'1px solid var(--rule-soft)'}}>
          <div className="ff-mono upper" style={{fontSize:9,letterSpacing:'.1em',color:'var(--ink-3)',marginBottom:10}}>AFFECTED ENTITY</div>
          <button onClick={openEntity}
            style={{display:'block',width:'100%',padding:'14px 16px',border:'1px solid var(--rule)',
              background:'var(--bg-2)',cursor:'pointer',textAlign:'left'}}
            onMouseEnter={(e)=>{e.currentTarget.style.background='var(--bg)'}}
            onMouseLeave={(e)=>{e.currentTarget.style.background='var(--bg-2)'}}>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'center'}}>
              <div style={{minWidth:0}}>
                <div style={{fontSize:14,fontWeight:600,letterSpacing:'-0.01em'}}>{a.entity_label}</div>
                {a.entity_secondary && <div className="ff-mono" style={{fontSize:11,color:'var(--ink-3)',marginTop:2}}>{a.entity_secondary}</div>}
              </div>
              <div className="ff-mono upper" style={{fontSize:10,letterSpacing:'.1em',color:'var(--ink-3)'}}>
                open {a.entity_type} →
              </div>
            </div>
            <div className="ff-mono" style={{fontSize:10,color:'var(--ink-3)',marginTop:8,letterSpacing:'.04em',display:'flex',gap:14,flexWrap:'wrap'}}>
              <span>ID: {a.entity_id}</span>
              {a.isrc && <span>ISRC: {a.isrc}</span>}
              {a.iswc && <span>ISWC: {a.iswc}</span>}
              {a.upc && <span>UPC: {a.upc}</span>}
            </div>
          </button>
        </div>

        {/* metadata */}
        <div style={{padding:'20px 24px',borderBottom:'1px solid var(--rule-soft)'}}>
          <div className="ff-mono upper" style={{fontSize:9,letterSpacing:'.1em',color:'var(--ink-3)',marginBottom:10}}>METADATA</div>
          <div style={{display:'grid',gridTemplateColumns:'120px 1fr',rowGap:10,columnGap:16,fontSize:12}}>
            <span className="ff-mono upper" style={{fontSize:10,color:'var(--ink-3)',letterSpacing:'.08em'}}>CATEGORY</span>
            <span style={{textTransform:'uppercase',letterSpacing:'.06em'}} className="ff-mono">{m.cat}</span>
            <span className="ff-mono upper" style={{fontSize:10,color:'var(--ink-3)',letterSpacing:'.08em'}}>SLA</span>
            <span className="ff-mono">{sla} day{sla>1?'s':''} {overSla && <span style={{color:'#a04432'}}>· breached</span>}</span>
            <span className="ff-mono upper" style={{fontSize:10,color:'var(--ink-3)',letterSpacing:'.08em'}}>REPORTED BY</span>
            <span className="ff-mono">{a.reported_by}</span>
            {a.reported_to && <>
              <span className="ff-mono upper" style={{fontSize:10,color:'var(--ink-3)',letterSpacing:'.08em'}}>ASSIGNED TO</span>
              <span className="ff-mono">{a.reported_to}</span>
            </>}
            {a.dsp_name && <>
              <span className="ff-mono upper" style={{fontSize:10,color:'var(--ink-3)',letterSpacing:'.08em'}}>SOURCE</span>
              <span className="ff-mono">{a.dsp_name}</span>
            </>}
            <span className="ff-mono upper" style={{fontSize:10,color:'var(--ink-3)',letterSpacing:'.08em'}}>CREATED</span>
            <span className="ff-mono">{a.created_at.toISOString().slice(0,10)}</span>
            {a.resolved_at && <>
              <span className="ff-mono upper" style={{fontSize:10,color:'var(--ink-3)',letterSpacing:'.08em'}}>RESOLVED</span>
              <span className="ff-mono">{a.resolved_at.toISOString().slice(0,10)} — {a.resolution}</span>
            </>}
          </div>
        </div>

        {/* suggested actions */}
        {(a.status === 'open' || a.status === 'investigating') && (
          <div style={{padding:'20px 24px',borderBottom:'1px solid var(--rule-soft)'}}>
            <div className="ff-mono upper" style={{fontSize:9,letterSpacing:'.1em',color:'var(--ink-3)',marginBottom:12}}>SUGGESTED ACTIONS</div>
            <div style={{display:'flex',flexDirection:'column',gap:0,border:'1px solid var(--rule)'}}>
              {actions.map((act, i) => (
                <button key={i} className="ff-mono upper"
                  style={{padding:'12px 14px',background:'transparent',border:0,
                    borderBottom: i < actions.length-1 ? '1px solid var(--rule-soft)':'none',
                    fontSize:11,letterSpacing:'.06em',cursor:'pointer',textAlign:'left',
                    color:'var(--ink)',display:'flex',justifyContent:'space-between',alignItems:'center'}}
                  onMouseEnter={(e)=>{e.currentTarget.style.background='var(--bg-2)'}}
                  onMouseLeave={(e)=>{e.currentTarget.style.background='transparent'}}>
                  <span>{i === 0 ? '◆' : '○'} {act}</span>
                  <span style={{color:'var(--ink-3)'}}>→</span>
                </button>
              ))}
            </div>
          </div>
        )}

        {/* footer */}
        <div style={{padding:'18px 24px',borderTop:'1px solid var(--rule)',display:'flex',gap:10,justifyContent:'flex-end',marginTop:'auto'}}>
          {a.status === 'resolved' || a.status === 'dismissed' ? (
            <>
              {a._promoted_task_id && (
                <button className="ff-mono upper"
                  onClick={() => { onClose(); go('inbox', { tab: 'tasks' }); }}
                  style={{padding:'8px 14px',background:'transparent',color:'var(--ink-2)',border:'1px solid var(--rule)',fontSize:11,letterSpacing:'.08em',cursor:'pointer'}}>
                  VIEW IN INBOX →
                </button>
              )}
              <button className="ff-mono upper" onClick={onClose}
                style={{padding:'8px 16px',background:'var(--ink)',color:'var(--bg)',border:0,fontSize:11,letterSpacing:'.08em',cursor:'pointer'}}>
                CLOSE
              </button>
            </>
          ) : (
            <>
              <button className="ff-mono upper"
                style={{padding:'8px 14px',background:'transparent',color:'var(--ink-3)',border:'1px solid var(--rule)',fontSize:11,letterSpacing:'.08em',cursor:'pointer'}}>
                DISMISS
              </button>
              <button className="ff-mono upper"
                style={{padding:'8px 14px',background:'transparent',color:'var(--ink)',border:'1px solid var(--ink)',fontSize:11,letterSpacing:'.08em',cursor:'pointer'}}>
                ASSIGN
              </button>
              {a._promoted_task_id ? (
                <button className="ff-mono upper"
                  onClick={() => { onClose(); go('inbox', { tab: 'tasks' }); }}
                  title="This issue was promoted to your Inbox"
                  style={{padding:'8px 14px',background:'transparent',color:'var(--ink-2)',border:'1px solid var(--ink-2)',fontSize:11,letterSpacing:'.08em',cursor:'pointer',display:'flex',alignItems:'center',gap:6}}>
                  <span style={{display:'inline-block',width:6,height:6,borderRadius:'50%',background:'var(--ink-2)'}}/>
                  IN INBOX · VIEW →
                </button>
              ) : (
                <button className="ff-mono upper"
                  onClick={() => {
                    if (window.__INBOX_PROMOTE) {
                      const t = window.__INBOX_PROMOTE(a);
                      if (t) {
                        a._promoted_task_id = t.id;
                        window.toast && window.toast(`Promoted to Inbox · due ${t.severity.replace('_',' ')}`, 'soft');
                        force();
                      }
                    }
                  }}
                  title="Add this issue to your Tasks Inbox with a due date"
                  style={{padding:'8px 14px',background:'transparent',color:'var(--ink)',border:'1px solid var(--rule)',fontSize:11,letterSpacing:'.08em',cursor:'pointer'}}>
                  ↑ PROMOTE TO INBOX
                </button>
              )}
              <button className="ff-mono upper"
                style={{padding:'8px 16px',background:'var(--ink)',color:'var(--bg)',border:0,fontSize:11,letterSpacing:'.08em',cursor:'pointer'}}>
                MARK RESOLVED
              </button>
            </>
          )}
        </div>
      </aside>
    </>
  );
}

// ─────────────────────────── main screen
function ScreenIssues({ go, payload }) {
  const [filters, setFilters] = useState({ status:'open', category:'all', severity:'all', q:'' });
  const [openId, setOpenId] = useState(null);
  const [page, setPage] = useState(0);
  const [collapsed, setCollapsed] = useState({}); // sev -> bool
  const PAGE_SIZE = 50;
  // reset page when filters change
  useEffect(() => { setPage(0); }, [filters.status, filters.category, filters.severity, filters.q]);

  const all = useMemo(() => (window.__buildAllAnomalies ? window.__buildAllAnomalies() : buildAnomalies()), []);

  // Deep-link from Inbox: open a specific anomaly drawer by id, and relax
  // filters so the row is actually in the visible set.
  useEffect(() => {
    const id = payload && payload.open_anomaly_id;
    if (!id) return;
    const a = all.find(x => x.id === id);
    if (!a) return;
    setFilters(f => ({
      ...f,
      status: a.status === f.status || f.status === 'all' ? f.status : 'all',
      category: 'all',
      severity: 'all',
      q: '',
    }));
    setOpenId(id);
  }, [payload && payload.open_anomaly_id, all]);

  const filtered = useMemo(() => {
    return all.filter(a => {
      if (filters.status !== 'all' && a.status !== filters.status) return false;
      if (filters.category !== 'all' && a.anomaly_meta.cat !== filters.category) return false;
      if (filters.severity !== 'all' && a.severity !== filters.severity) return false;
      if (filters.q) {
        const q = filters.q.toLowerCase();
        const hay = [a.description, a.entity_label, a.entity_secondary, a.isrc, a.iswc, a.upc, a.dsp_name, a.anomaly_meta.label]
          .filter(Boolean).join(' ').toLowerCase();
        if (!hay.includes(q)) return false;
      }
      return true;
    }).sort((x,y) => {
      // primary: severity rank, secondary: age desc
      const s = SEVERITY[x.severity].rank - SEVERITY[y.severity].rank;
      if (s !== 0) return s;
      return y.age_days - x.age_days;
    });
  }, [all, filters]);

  const counts = useMemo(() => {
    const byStatus = {};
    all.forEach(a => byStatus[a.status] = (byStatus[a.status]||0) + 1);
    return { byStatus, total: filtered.length };
  }, [all, filtered]);

  // Top-level metrics
  const metrics = useMemo(() => {
    const open = all.filter(a => a.status === 'open' || a.status === 'investigating');
    const breached = open.filter(a => a.age_days > SEVERITY[a.severity].sla);
    const critical = open.filter(a => a.severity === 'critical');
    const resolved30d = all.filter(a => a.status === 'resolved' && a.resolved_at && a.age_days <= 30);
    return { openCount: open.length, breachedCount: breached.length, criticalCount: critical.length, resolvedCount: resolved30d.length };
  }, [all]);

  const openItem = openId ? all.find(a => a.id === openId) : null;

  return (
    <>
      {/* ─────── Page header — canonical <PageHeader> */}
      <div style={{borderBottom:'1px solid var(--rule)',paddingBottom:24,marginBottom:24}}>
        <PageHeader
          eyebrow="QUALITY · ANOMALY INBOX"
          title="issues"
          highlight="Issues"
          sub="Every flagged anomaly across the catalog — DSP rate variances, missing IDs, split mismatches, CWR rejections, QC failures, ownership conflicts, black-box pools, expiring agreements. Sorted by severity then age."
          actions={
            <>
              <button className="ff-mono upper"
                style={{padding:'8px 14px',background:'transparent',color:'var(--ink-3)',border:'1px solid var(--rule)',fontSize:11,letterSpacing:'.08em',cursor:'pointer',whiteSpace:'nowrap',flexShrink:0}}>
                EXPORT CSV
              </button>
              <button className="ff-mono upper"
                style={{padding:'8px 14px',background:'var(--ink)',color:'var(--bg)',border:0,fontSize:11,letterSpacing:'.08em',cursor:'pointer',whiteSpace:'nowrap',flexShrink:0}}>
                RUN DETECTOR
              </button>
            </>
          }
        />
      </div>

      {/* ─────── Workspace surfaces — deep-links to operational/admin screens that
          live alongside Issues but warrant their own pages. Issues is the unified
          inbox; these are the focused work surfaces.  */}
      <div style={{display:'flex',gap:0,border:'1px solid var(--rule)',marginBottom:24,background:'var(--bg)'}}>
        {[
          { k:'claims',      l:'Claims & disputes', sub:'rights queue',          go:()=>go('claims') },
          { k:'cwr',         l:'CWR transmissions', sub:'CWR · DDEX out',        go:()=>go('cwr') },
          { k:'blackbox',    l:'Black-box',         sub:'unallocated pools',     go:()=>go('blackbox') },
          { k:'copyright',   l:'Copyright',         sub:'registrations',         go:()=>go('copyright') },
          { k:'subpubs',     l:'Sub-publishers',    sub:'territorial deals',     go:()=>go('subpubs') },
          { k:'conformance', l:'Import & Conform',  sub:'data migration',        go:()=>go('conformance') },
          { k:'audit',       l:'Audit log',         sub:'workspace activity',    go:()=>go('audit') },
        ].map((p, i, arr) => (
          <button key={p.k} onClick={p.go}
            style={{flex:1,padding:'14px 18px',
              borderRight: i < arr.length - 1 ? '1px solid var(--rule)' : 'none',
              background:'transparent',cursor:'pointer',textAlign:'left',
              display:'flex',flexDirection:'column',gap:3}}
            onMouseEnter={(e)=>e.currentTarget.style.background='var(--bg-2)'}
            onMouseLeave={(e)=>e.currentTarget.style.background='transparent'}>
            <span className="ff-mono upper" style={{fontSize:9,letterSpacing:'.12em',color:'var(--ink-3)'}}>{p.sub}</span>
            <span style={{fontSize:13,fontWeight:600,letterSpacing:'-0.01em',color:'var(--ink)'}}>{p.l} →</span>
          </button>
        ))}
      </div>

      {/* ─────── KPI strip — CRITICAL dropped (already encoded in distribution chart) */}
      <div style={{display:'flex',border:'1px solid var(--rule)',marginBottom:24}}>
        <Stat label="OPEN ISSUES" value={metrics.openCount.toLocaleString()} sub="needs review" />
        <Stat label="SLA BREACHED" value={metrics.breachedCount.toLocaleString()} sub="past resolution window" accent={metrics.breachedCount > 0 ? '#a04432' : 'var(--ink)'} />
        <Stat label="RESOLVED (30D)" value={metrics.resolvedCount.toLocaleString()} sub="closed in last 30 days" last />
      </div>

      {/* ─────── Distribution chart */}
      <CategoryDistribution items={all}/>

      {/* ─────── Filter bar */}
      <FilterBar filters={filters} setFilters={setFilters} types={ANOMALY_TYPES} counts={counts}/>

      {/* ─────── Issues list */}
      {(() => {
        // hide STATUS col when a single status is filtered (every row would say the same thing)
        const hideStatus = filters.status !== 'all';
        // group by severity when sort is severity-default and severity filter is 'all'
        const groupBySev = filters.severity === 'all';
        // pagination
        const total = filtered.length;
        const pageStart = page * PAGE_SIZE;
        const pageEnd = Math.min(pageStart + PAGE_SIZE, total);
        const pageRows = filtered.slice(pageStart, pageEnd);
        const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE));
        // build groups within current page
        const groups = [];
        if (groupBySev) {
          const order = ['critical','high','medium','low'];
          // counts across the FULL filtered set, not just this page (so headers tell the truth)
          const fullCounts = order.reduce((acc, k) => { acc[k] = filtered.filter(a => a.severity === k).length; return acc; }, {});
          order.forEach(sev => {
            const rows = pageRows.filter(a => a.severity === sev);
            if (rows.length === 0 && fullCounts[sev] === 0) return;
            groups.push({ key: sev, label: SEVERITY[sev].label, color: SEVERITY[sev].color, total: fullCounts[sev], rows });
          });
        } else {
          groups.push({ key: 'all', rows: pageRows });
        }
        const cols = hideStatus
          ? '80px 110px 1.6fr 1.4fr 120px 70px'
          : '80px 110px 1.6fr 1.4fr 120px 90px 70px';
        const headers = hideStatus
          ? ['SEV','TYPE','DESCRIPTION','ENTITY','AGE / SLA','']
          : ['SEV','TYPE','DESCRIPTION','ENTITY','AGE / SLA','STATUS',''];
        return (
          <div style={{border:'1px solid var(--rule)',background:'var(--bg)'}}>
            <div style={{display:'grid',gridTemplateColumns:cols,gap:0,
              padding:'10px 14px',borderBottom:'1px solid var(--rule)',background:'var(--bg-2)'}}>
              {headers.map((h,i) => (
                <span key={i} className="ff-mono upper" style={{fontSize:9,color:'var(--ink-3)',letterSpacing:'.1em',
                  textAlign: i===headers.length-1 ? 'right' : 'left'}}>{h}</span>
              ))}
            </div>
            {filtered.length === 0 ? (
              <div style={{padding:'48px 24px',textAlign:'center',color:'var(--ink-3)'}}>
                <div className="ff-mono upper" style={{fontSize:11,letterSpacing:'.1em'}}>NO ISSUES MATCH CURRENT FILTERS</div>
                <button onClick={()=>setFilters({status:'all',category:'all',severity:'all',q:''})}
                  className="ff-mono upper" style={{marginTop:14,fontSize:10,letterSpacing:'.08em',color:'var(--ink)',background:'none',border:0,cursor:'pointer'}}>
                  clear filters →
                </button>
              </div>
            ) : (
              groups.map((g, gi) => {
                if (g.key === 'all') {
                  return g.rows.map((a, i) => (
                    <IssueRow key={a.id} a={a} hideStatus={hideStatus}
                      isLast={i === g.rows.length - 1 && gi === groups.length - 1}
                      onOpen={()=>setOpenId(a.id)}/>
                  ));
                }
                const isCollapsed = !!collapsed[g.key];
                return (
                  <React.Fragment key={g.key}>
                    <div onClick={()=>setCollapsed(c=>({...c,[g.key]:!c[g.key]}))}
                      style={{display:'flex',alignItems:'center',gap:10,
                        padding:'14px 14px 12px',
                        borderTop: gi === 0 ? 'none' : '1px solid var(--rule)',
                        borderBottom:'1px solid var(--rule-soft)',
                        background:'var(--bg-2)',cursor:'pointer',userSelect:'none'}}>
                      <span className="ff-mono" style={{fontSize:10,color:'var(--ink-3)',width:10,display:'inline-block'}}>
                        {isCollapsed ? '▸' : '▾'}
                      </span>
                      <span style={{width:6,height:6,background:g.color,display:'inline-block'}}/>
                      <span className="ff-mono upper" style={{fontSize:10,letterSpacing:'.12em',color:g.color,fontWeight:600}}>
                        {g.label}
                      </span>
                      <span className="ff-mono" style={{fontSize:10,color:'var(--ink-3)',letterSpacing:'.06em'}}>
                        · {g.total} {g.total === 1 ? 'issue' : 'issues'}
                        {g.rows.length < g.total && ` (${g.rows.length} on page)`}
                      </span>
                    </div>
                    {!isCollapsed && g.rows.map((a, i) => (
                      <IssueRow key={a.id} a={a} hideStatus={hideStatus}
                        isLast={i === g.rows.length - 1 && gi === groups.length - 1}
                        onOpen={()=>setOpenId(a.id)}/>
                    ))}
                  </React.Fragment>
                );
              })
            )}
            {/* Pagination footer */}
            {total > PAGE_SIZE && (
              <div style={{padding:'10px 14px',borderTop:'1px solid var(--rule)',
                display:'flex',justifyContent:'space-between',alignItems:'center',background:'var(--bg-2)'}}>
                <span className="ff-mono" style={{fontSize:11,color:'var(--ink-3)',letterSpacing:'.04em'}}>
                  {(pageStart+1).toLocaleString()}–{pageEnd.toLocaleString()} of {total.toLocaleString()}
                </span>
                <div style={{display:'flex',gap:6,alignItems:'center'}}>
                  <button onClick={()=>setPage(p=>Math.max(0,p-1))} disabled={page===0}
                    className="ff-mono upper"
                    style={{padding:'4px 10px',fontSize:10,letterSpacing:'.08em',
                      background:'transparent',color: page===0 ? 'var(--ink-3)':'var(--ink)',
                      border:'1px solid var(--rule)',cursor: page===0?'not-allowed':'pointer'}}>
                    ← PREV
                  </button>
                  <span className="ff-mono" style={{fontSize:11,color:'var(--ink-2)',letterSpacing:'.04em',padding:'0 8px'}}>
                    page {page+1} of {pageCount}
                  </span>
                  <button onClick={()=>setPage(p=>Math.min(pageCount-1,p+1))} disabled={page>=pageCount-1}
                    className="ff-mono upper"
                    style={{padding:'4px 10px',fontSize:10,letterSpacing:'.08em',
                      background:'transparent',color: page>=pageCount-1 ? 'var(--ink-3)':'var(--ink)',
                      border:'1px solid var(--rule)',cursor: page>=pageCount-1?'not-allowed':'pointer'}}>
                    NEXT →
                  </button>
                </div>
              </div>
            )}
          </div>
        );
      })()}

      {/* ─────── Drawer */}
      {openItem && <IssueDrawer a={openItem} onClose={()=>setOpenId(null)} go={go}/>}
    </>
  );
}

// expose
window.ScreenIssues = ScreenIssues;
window.__ANOMALY_TYPES = ANOMALY_TYPES;
window.__SEVERITY = SEVERITY;
window.__buildAnomalies = buildAnomalies;
})();
