// royalty-optimize.jsx — Royalty Optimization Strategies
// ─────────────────────────────────────────────────────────────────
// Scans real statement data (window.__STMT_INDEX) + catalog state and
// surfaces ranked strategies to lift earnings. Each strategy carries:
//   • $ projected annual lift
//   • effort score (low/medium/high)
//   • confidence
//   • specific evidence rows (which statements/sources/works drive it)
//   • next actions
//
// 12 strategy families, real heuristics where data exists, deterministic
// projections where it doesn't:
//
//   01 BLACKBOX RECOVERY      — unmatched income lines on real statements
//   02 LATE-FEE RECOVERY      — late statements past contractual cadence
//   03 FX HEDGE               — unhedged non-USD exposure
//   04 RECIPROCITY GAPS       — territories with weak PRO collection
//   05 DSP MIX REBALANCE      — under-exploited platforms vs catalog fit
//   06 NEIGHBORING RIGHTS     — performer-side income leakage
//   07 SYNC CATALOG ACTIVATION— pitch-ready works with no licenses
//   08 MICROSYNC / UGC        — TikTok/Reels claim recovery
//   09 MECHANICAL TRUE-UP     — MLC/HFA opt-out / underclaimed shares
//   10 PUBLIC PERFORMANCE     — venue/broadcast undercollection
//   11 STATEMENT AUDIT        — anomaly clusters worth a deep audit
//   12 CADENCE NEGOTIATION    — push slow sources to monthly cadence
//
// Triggered by `astro-open-royalty-optimizer` event; mounted globally.
// Adds an "Optimize" pill to the Earnings hero.
//
// EXPORT: window.RoyaltyOptimizer (drawer)
//         window.RoyaltyOptimizerEngine (pure analysis)
// ─────────────────────────────────────────────────────────────────
(function () {
  if (typeof window === 'undefined' || !window.React) return;
  const _S = React.useState, _M = React.useMemo, _E = React.useEffect;

  const fmtUsd = (n) => '$' + (n >= 1_000_000 ? (n/1_000_000).toFixed(2) + 'M' : n >= 1000 ? (n/1000).toFixed(1) + 'k' : Math.round(n).toLocaleString());
  const fmtPct = (n) => Math.round(n*100) + '%';

  function Mono({ children, upper, size, color, style, ...rest }) {
    return <span className={'ff-mono' + (upper?' upper':'')} style={{ fontSize: size||11, color: color||'var(--ink)', letterSpacing: upper?'.08em':0, ...style }} {...rest}>{children}</span>;
  }

  // ─── Deterministic RNG seeded by content fingerprint ───────────
  function pseed(seed) {
    let s = 0; for (let i = 0; i < seed.length; i++) s = (s * 31 + seed.charCodeAt(i)) | 0;
    let a = s ^ 0x9e3779b9, b = s ^ 0xdeadbeef, c = s ^ 0x41c6ce57, d = s ^ 0x6b79f5d3;
    return function () {
      a |= 0; b |= 0; c |= 0; d |= 0;
      const t = (((a + b) | 0) + d) | 0; d = (d + 1) | 0;
      a = b ^ (b >>> 9); b = (c + (c << 3)) | 0; c = (c << 21 | c >>> 11);
      c = (c + t) | 0;
      return (t >>> 0) / 4294967296;
    };
  }

  // ─── Engine: scan statements + catalog, return ranked strategies
  function analyze() {
    const idx = window.__STMT_INDEX;
    const recs = window.RECORDINGS || [];
    const stmts = (idx && idx.statements) || [];

    // Build aggregates
    const grossTotal = stmts.reduce((s, r) => s + (r.grossUsd || 0), 0);
    const grossAnnualized = grossTotal * (4 / Math.max(1, distinctPeriods(stmts)));  // annualize from quarters present
    const rng = pseed('royopt·' + stmts.length + '·' + recs.length + '·' + Math.round(grossTotal));

    const strategies = [];

    // ── 01 BLACKBOX RECOVERY — unmatched lines on real statements
    {
      let unmatched = 0, unmatchedUsd = 0, totalLines = 0;
      const offenders = [];
      stmts.forEach(r => {
        const lines = (r.lines || []).length;
        const matched = r.matchedLineCount || 0;
        const u = lines - matched;
        if (u > 0) {
          totalLines += lines;
          unmatched += u;
          // estimate $ as proportional of statement gross
          const usd = lines ? (u / lines) * (r.grossUsd || 0) : 0;
          unmatchedUsd += usd;
          if (u >= 5) offenders.push({ src: r.sourceName || r.sourceId, period: r.period, count: u, usd });
        }
      });
      offenders.sort((a, b) => b.usd - a.usd);
      const recoverable = unmatchedUsd * 0.62;  // typical recovery rate after audit + claim
      strategies.push({
        id: 'blackbox',
        family: 'recovery',
        title: 'Black-box claim recovery',
        kicker: `${unmatched.toLocaleString()} unmatched lines across ${stmts.length} statements`,
        annualLift: Math.round(recoverable * (4 / Math.max(1, distinctPeriods(stmts)))),
        effort: unmatched > 200 ? 'medium' : 'low',
        conf: 0.84,
        evidence: offenders.slice(0, 5).map(o => ({ label: `${o.src} · ${o.period}`, value: `${o.count} lines · ${fmtUsd(o.usd)} at risk` })),
        actions: [
          'Run BB Rank ML on unmatched lines to surface high-confidence catalog matches',
          'File black-box claim requests with offending sources before holdback escheat (typically 36 months)',
          'Wire ISWC/ISRC into outbound CWR registrations to prevent future unmatched',
        ],
      });
    }

    // ── 02 LATE-FEE RECOVERY — sources past contractual cadence
    {
      const today = new Date('2026-04-30');
      const late = [];
      const periods = window.__STMT_PERIODS || [];
      stmts.forEach(r => {
        const per = periods.find(p => p.id === r.period);
        if (!per || !r.processedDate) return;
        const expected = new Date(per.closed);
        expected.setDate(expected.getDate() + 90);
        const proc = new Date(r.processedDate);
        const lateDays = Math.round((proc - expected) / 86400000);
        if (lateDays > 14) late.push({ src: r.sourceName || r.sourceId, lateDays, usd: r.grossUsd || 0 });
      });
      late.sort((a, b) => b.lateDays - a.lateDays);
      // Late-fee clauses typically 1.5%/month, capped 18%/yr
      const lift = late.reduce((s, l) => s + l.usd * Math.min(0.18, (l.lateDays / 30) * 0.015), 0);
      if (late.length > 0) strategies.push({
        id: 'latefee',
        family: 'recovery',
        title: 'Late-fee invoicing',
        kicker: `${late.length} statements past contractual cadence`,
        annualLift: Math.round(lift * (4 / Math.max(1, distinctPeriods(stmts)))),
        effort: 'low',
        conf: 0.71,
        evidence: late.slice(0, 4).map(l => ({ label: l.src, value: `${l.lateDays}d late · ${fmtUsd(l.usd)} statement` })),
        actions: [
          'Generate late-fee invoices for the 4 oldest offenders',
          'Reference admin-deal §6.2 (1.5%/mo accrual on overdue distributions)',
          'Add cadence SLA to next contract renewal cycle',
        ],
      });
    }

    // ── 03 FX HEDGE — unhedged non-USD exposure
    {
      const byCcy = {};
      stmts.forEach(r => {
        if ((r.currency || 'USD') === 'USD') return;
        if (!byCcy[r.currency]) byCcy[r.currency] = { ccy: r.currency, native: 0, usd: 0, count: 0 };
        byCcy[r.currency].usd += r.grossUsd || 0;
        byCcy[r.currency].count += 1;
      });
      const nonUsd = Object.values(byCcy).reduce((s, c) => s + c.usd, 0);
      const fxPct = grossTotal ? nonUsd / grossTotal : 0;
      // Typical FX volatility cost: ~3-5% of unhedged exposure annually
      const lift = nonUsd * 0.04 * (4 / Math.max(1, distinctPeriods(stmts)));
      if (nonUsd > 0) strategies.push({
        id: 'fxhedge',
        family: 'finance',
        title: 'FX hedge program',
        kicker: `${fmtPct(fxPct)} of gross arrives non-USD · ${Object.keys(byCcy).length} currencies`,
        annualLift: Math.round(lift),
        effort: 'medium',
        conf: 0.68,
        evidence: Object.values(byCcy).sort((a,b) => b.usd - a.usd).slice(0, 4).map(c => ({
          label: c.ccy, value: `${fmtUsd(c.usd * (4 / Math.max(1, distinctPeriods(stmts))))} annualized · ${c.count} statements`,
        })),
        actions: [
          'Open FX forward contracts on EUR/GBP exposure (>$50k threshold)',
          'Set quarterly hedge ratio of 60–75% based on confirmed pipeline',
          'Move JPY/AUD inflows to natural hedge by paying SubPub admin in same currency',
        ],
      });
    }

    // ── 04 RECIPROCITY GAPS — territories under-collecting vs streaming share
    {
      // Target: territories should track DSP share within ±20%
      const dspShare = { US: 0.36, GB: 0.09, DE: 0.05, FR: 0.025, JP: 0.08, BR: 0.04, MX: 0.025, CA: 0.025, AU: 0.02, ES: 0.02 };
      const proCollection = collectionByTerritory(stmts);
      const gaps = [];
      Object.keys(dspShare).forEach(t => {
        const expected = grossAnnualized * dspShare[t] * 0.18;  // ~18% of streaming → public-perf revenue
        const actual = proCollection[t] || 0;
        const gap = expected - actual;
        if (gap > expected * 0.30) gaps.push({ t, expected, actual, gap });
      });
      gaps.sort((a, b) => b.gap - a.gap);
      const lift = gaps.reduce((s, g) => s + g.gap, 0) * 0.55;  // 55% recoverable through reciprocity audit
      if (gaps.length > 0) strategies.push({
        id: 'reciprocity',
        family: 'collection',
        title: 'PRO reciprocity audit',
        kicker: `${gaps.length} territories collecting <70% of expected`,
        annualLift: Math.round(lift),
        effort: 'high',
        conf: 0.62,
        evidence: gaps.slice(0, 4).map(g => ({
          label: territoryName(g.t),
          value: `${fmtUsd(g.actual)} actual vs ${fmtUsd(g.expected)} expected · gap ${fmtUsd(g.gap)}`,
        })),
        actions: [
          'Cross-check ASCAP/BMI/PRS reciprocity reports against DSP territory mix',
          'File catch-up registrations with affiliated society for the gap territory',
          'Escalate via CISAC IPI cross-society audit if gap persists 2 quarters',
        ],
      });
    }

    // ── 05 DSP MIX REBALANCE — under-exploited platforms
    {
      const dspMix = lineLevelDSPMix(stmts);
      const idealMix = { 'Spotify': 0.34, 'Apple Music': 0.20, 'YouTube Music': 0.16, 'Amazon Music': 0.14, 'Tidal': 0.04, 'Deezer': 0.04, 'Pandora': 0.05, 'SoundCloud': 0.03 };
      const gaps = [];
      Object.keys(idealMix).forEach(d => {
        const have = dspMix[d] || 0;
        const should = idealMix[d];
        if (have < should * 0.5 && grossAnnualized > 10000) {
          const lift = grossAnnualized * (should - have);
          gaps.push({ d, have, should, lift });
        }
      });
      gaps.sort((a, b) => b.lift - a.lift);
      const total = gaps.reduce((s, g) => s + g.lift, 0) * 0.6;
      if (gaps.length > 0) strategies.push({
        id: 'dspmix',
        family: 'distribution',
        title: 'DSP distribution rebalance',
        kicker: `${gaps.length} platforms under-indexing vs market share`,
        annualLift: Math.round(total),
        effort: 'low',
        conf: 0.74,
        evidence: gaps.slice(0, 4).map(g => ({
          label: g.d,
          value: `${fmtPct(g.have)} actual vs ${fmtPct(g.should)} target · +${fmtUsd(g.lift)} potential`,
        })),
        actions: [
          'Confirm distribution coverage with aggregator (DistroKid / Believe / The Orchard)',
          'Submit to platform editorial pitch boxes — most accept 14d before release',
          'Audit catalog for missing platforms quarterly via release-platforms screen',
        ],
      });
    }

    // ── 06 NEIGHBORING RIGHTS — performer income leakage
    {
      const recCount = recs.length;
      // Roughly 8–12% of master streaming revenue should come back as neighboring
      const expected = grossAnnualized * 0.10;
      const sxRow = stmts.filter(r => /soundexchange|sx|ppl|ssa/i.test(r.sourceName || r.sourceId || ''));
      const actual = sxRow.reduce((s, r) => s + (r.grossUsd || 0), 0) * (4 / Math.max(1, distinctPeriods(stmts)));
      const gap = expected - actual;
      if (gap > 5000 && recCount > 0) strategies.push({
        id: 'neighboring',
        family: 'collection',
        title: 'Neighboring rights collection',
        kicker: `${fmtUsd(actual)} collected vs ${fmtUsd(expected)} expected for ${recCount} recordings`,
        annualLift: Math.round(gap * 0.65),
        effort: 'medium',
        conf: 0.67,
        evidence: [
          { label: 'SoundExchange (US)', value: actual > 0 ? `Active · ${fmtUsd(actual)}/yr` : 'Not registered — leaving 100% on the table' },
          { label: 'PPL (UK)', value: 'Reciprocal society — auto-collects via SX' },
          { label: 'Untapped territories', value: 'GVL (DE), SCPP (FR), SoundExchange Brazil' },
        ],
        actions: [
          'Register all featured + non-featured performers with SoundExchange',
          'Submit territory-specific neighboring rights claims via collection society network',
          'Verify ISRC-to-performer mapping in catalog (recording-platforms screen)',
        ],
      });
    }

    // ── 07 SYNC CATALOG ACTIVATION
    {
      const pitchable = recs.filter(r => {
        const f = window.PredictEngine ? window.PredictEngine.getFeatures(r) : null;
        return f && f.audio.energy > 0.6 && f.audio.instrumentalness > 0.15;
      }).length;
      const lift = pitchable * 1800 * 0.04;  // $1.8k avg fee × 4% placement rate
      if (pitchable > 5) strategies.push({
        id: 'sync',
        family: 'distribution',
        title: 'Sync catalog activation',
        kicker: `${pitchable} works fit common sync archetypes but show no licensing activity`,
        annualLift: Math.round(lift),
        effort: 'medium',
        conf: 0.59,
        evidence: [
          { label: 'Trailer/Cinematic fit', value: `${Math.round(pitchable * 0.4)} works · avg fee $25k–$80k` },
          { label: 'Tech/Auto Ad fit', value: `${Math.round(pitchable * 0.3)} works · avg fee $40k–$120k` },
          { label: 'TV/Episodic fit', value: `${Math.round(pitchable * 0.3)} works · avg fee $5k–$25k` },
        ],
        actions: [
          'Build instrumental + clean stem deliverables for top-30 ranked works',
          'Pitch to 4–6 sync agencies (Sentric, Position, Songtradr, Musicbed)',
          'Run Predict→Sync screen quarterly to refresh archetype rankings',
        ],
      });
    }

    // ── 08 MICROSYNC / UGC
    {
      const lift = grossAnnualized * 0.04;  // typical UGC uplift after claiming
      strategies.push({
        id: 'ugc',
        family: 'collection',
        title: 'UGC / micro-sync claim recovery',
        kicker: 'TikTok, Reels, YT Shorts royalty pools — typical 3–6% of streaming income',
        annualLift: Math.round(lift),
        effort: 'low',
        conf: 0.72,
        evidence: [
          { label: 'TikTok Commercial Music Library', value: 'Verify all releases claimed via aggregator' },
          { label: 'Meta (FB/IG/Reels)', value: 'Audio Library deal — auto-claim if registered' },
          { label: 'YouTube Content ID', value: 'Reference files for all masters; sync to publisher CMS' },
        ],
        actions: [
          'Verify Content ID coverage on every master ISRC',
          'Sync writer shares to publisher CMS for split-claim flows',
          'Audit unclaimed UGC pool via DistroKid/CD Baby quarterly report',
        ],
      });
    }

    // ── 09 MECHANICAL TRUE-UP
    {
      const lift = grossAnnualized * 0.018;
      strategies.push({
        id: 'mechanical',
        family: 'recovery',
        title: 'MLC mechanical true-up audit',
        kicker: 'Phonorecords IV settlement + black-box pool · MLC opt-in coverage',
        annualLift: Math.round(lift),
        effort: 'medium',
        conf: 0.66,
        evidence: [
          { label: 'MLC unmatched pool', value: 'US$424M+ accumulated as of 2025 distribution' },
          { label: 'Phonorecords IV', value: 'Streaming mechanical rate increase (effective 2023–27)' },
          { label: 'Coverage check', value: 'Use mlc-sync screen to surface unmatched works' },
        ],
        actions: [
          'Run mlc-sync screen — claim unmatched works in MLC public database',
          'Verify Phonorecords IV true-up has been applied to historical statements',
          'Cross-check HFA admin coverage for direct-license catalog',
        ],
      });
    }

    // ── 10 PUBLIC PERFORMANCE — venue/broadcast
    {
      const lift = grossAnnualized * 0.025;
      strategies.push({
        id: 'venue',
        family: 'collection',
        title: 'Venue + broadcast performance audit',
        kicker: 'Live, broadcast, and bg-music undercollection across PRO networks',
        annualLift: Math.round(lift),
        effort: 'high',
        conf: 0.55,
        evidence: [
          { label: 'Live performance setlists', value: 'Cross-reference tour data with PRO live filings' },
          { label: 'Sync-cleared broadcasts', value: 'TV/film usage often missed by cue-sheet workflow' },
          { label: 'Background music', value: 'Hospitality, retail, fitness — 1–2% catalog uplift' },
        ],
        actions: [
          'Submit 2024–25 tour setlists via ASCAP OnStage / BMI Live',
          'Audit sync-licensed works for matching cue sheets',
          'Engage BMG/Reservoir-style admin for venue/bg coverage',
        ],
      });
    }

    // ── 11 STATEMENT AUDIT — anomaly clusters
    {
      const anomCount = stmts.reduce((s, r) => s + Math.max(0, ((r.lines || []).length - (r.matchedLineCount || 0))), 0);
      const highAnom = stmts.filter(r => ((r.lines || []).length - (r.matchedLineCount || 0)) > 10);
      if (highAnom.length > 0) {
        const lift = highAnom.reduce((s, r) => s + (r.grossUsd || 0), 0) * 0.04;
        strategies.push({
          id: 'audit',
          family: 'recovery',
          title: 'Targeted statement audit',
          kicker: `${highAnom.length} statements show >10 unmatched lines — audit ROI threshold met`,
          annualLift: Math.round(lift * (4 / Math.max(1, distinctPeriods(stmts)))),
          effort: 'high',
          conf: 0.61,
          evidence: highAnom.slice(0, 4).map(r => ({
            label: r.sourceName || r.sourceId,
            value: `${(r.lines||[]).length - (r.matchedLineCount||0)} unmatched · ${fmtUsd(r.grossUsd || 0)} statement`,
          })),
          actions: [
            'Engage royalty audit firm (Massarsky, Citrin Cooperman, Prager Metis)',
            'Request raw line-level data + rate sheets for the audit period',
            'Typical 18–24 month engagement; 1.5–4% recovery is industry baseline',
          ],
        });
      }
    }

    // ── 12 CADENCE NEGOTIATION
    {
      const slow = stmts.filter(r => {
        const per = (window.__STMT_PERIODS || []).find(p => p.id === r.period);
        return per && r.processedDate && (new Date(r.processedDate) - new Date(per.closed)) > 100 * 86400000;
      });
      const slowSrcs = uniq(slow.map(r => r.sourceName || r.sourceId));
      const lift = slow.reduce((s, r) => s + (r.grossUsd || 0), 0) * 0.025;  // float interest + earlier deployment
      if (slowSrcs.length > 0) strategies.push({
        id: 'cadence',
        family: 'finance',
        title: 'Statement cadence negotiation',
        kicker: `${slowSrcs.length} sources running 100+ days past period close`,
        annualLift: Math.round(lift * (4 / Math.max(1, distinctPeriods(stmts)))),
        effort: 'medium',
        conf: 0.58,
        evidence: slowSrcs.slice(0, 5).map(s => ({ label: s, value: 'Push to monthly via subpub renewal' })),
        actions: [
          'Add monthly cadence + 60-day SLA to next admin/subpub renewal',
          'Value: float earnings (~2.5% APY) + earlier writer advances',
          'Tier 1 sources (Spotify/Apple) already monthly — focus negotiation on PROs/MROs',
        ],
      });
    }

    // Rank by impact-to-effort ratio
    const effortScore = { low: 1, medium: 2, high: 3.5 };
    strategies.forEach(s => {
      s.score = (s.annualLift * s.conf) / effortScore[s.effort];
    });
    strategies.sort((a, b) => b.score - a.score);

    const totalLift = strategies.reduce((s, x) => s + x.annualLift, 0);
    const topThree = strategies.slice(0, 3).reduce((s, x) => s + x.annualLift, 0);

    return { strategies, totalLift, topThree, grossAnnualized, n: strategies.length };
  }

  // ─── helpers ───────────────────────────────────────────────────
  function distinctPeriods(stmts) {
    const set = new Set();
    stmts.forEach(r => set.add(r.period));
    return set.size;
  }
  function uniq(a) { return Array.from(new Set(a)); }
  function territoryName(t) {
    return ({US:'United States', GB:'United Kingdom', DE:'Germany', FR:'France', JP:'Japan', BR:'Brazil', MX:'Mexico', CA:'Canada', AU:'Australia', ES:'Spain'})[t] || t;
  }
  function collectionByTerritory(stmts) {
    const out = {};
    stmts.forEach(r => {
      (r.lines || []).forEach(l => {
        const t = (l.territory || 'US').toUpperCase().slice(0, 2);
        out[t] = (out[t] || 0) + (l.amountUsd || l.usd || 0);
      });
    });
    return out;
  }
  function lineLevelDSPMix(stmts) {
    const out = {}; let total = 0;
    stmts.forEach(r => {
      (r.lines || []).forEach(l => {
        const dsp = (l.dsp || l.platform || '').trim();
        if (!dsp) return;
        out[dsp] = (out[dsp] || 0) + (l.amountUsd || l.usd || 0);
        total += (l.amountUsd || l.usd || 0);
      });
    });
    if (!total) return {};
    Object.keys(out).forEach(k => out[k] = out[k] / total);
    return out;
  }

  // ─── UI ────────────────────────────────────────────────────────
  const FAMILY_COLOR = {
    recovery:     '#a32a18',
    finance:      '#0a3a8c',
    collection:   '#0a8754',
    distribution: '#7a3a8c',
  };
  const EFFORT_COLOR = { low: '#0a8754', medium: '#d4881f', high: '#a32a18' };

  function StrategyCard({ s, expanded, onToggle }) {
    return (
      <div style={{
        border: '1px solid var(--rule)',
        borderLeft: '3px solid ' + FAMILY_COLOR[s.family],
        background: expanded ? 'var(--bg-2)' : 'var(--paper)',
        marginBottom: 10,
      }}>
        <div onClick={onToggle} style={{
          display: 'grid', gridTemplateColumns: 'auto 1fr auto auto auto',
          gap: 16, alignItems: 'center', padding: '14px 18px', cursor: 'pointer',
        }}>
          <Mono upper size={9} color={FAMILY_COLOR[s.family]} style={{ width: 76 }}>
            {s.family.toUpperCase()}
          </Mono>
          <div>
            <div style={{ fontSize: 14, fontWeight: 500, letterSpacing: '-0.005em' }}>{s.title}</div>
            <div style={{ fontSize: 11, color: 'var(--ink-2)', marginTop: 3 }}>{s.kicker}</div>
          </div>
          <div style={{ textAlign: 'right' }}>
            <Mono upper size={8} color="var(--ink-3)">PROJ ANNUAL</Mono>
            <div style={{ fontSize: 16, fontWeight: 600, fontFamily: 'var(--ff-mono, monospace)', color: 'var(--ink)', letterSpacing: '-0.01em' }}>
              {fmtUsd(s.annualLift)}
            </div>
          </div>
          <div style={{ textAlign: 'right', minWidth: 70 }}>
            <Mono upper size={8} color="var(--ink-3)">EFFORT</Mono>
            <div style={{ fontSize: 11, fontWeight: 500, color: EFFORT_COLOR[s.effort], textTransform: 'uppercase', letterSpacing: '0.05em' }}>
              {s.effort}
            </div>
          </div>
          <div style={{ textAlign: 'right', minWidth: 50 }}>
            <Mono upper size={8} color="var(--ink-3)">CONF</Mono>
            <div style={{ fontSize: 11, fontFamily: 'var(--ff-mono, monospace)', fontWeight: 500 }}>
              {Math.round(s.conf * 100)}%
            </div>
          </div>
        </div>
        {expanded && (
          <div style={{ padding: '0 18px 18px', borderTop: '1px solid var(--rule-soft)' }}>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24, paddingTop: 14 }}>
              <div>
                <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 8 }}>EVIDENCE</Mono>
                {s.evidence.map((e, i) => (
                  <div key={i} style={{ padding: '8px 0', borderBottom: '1px solid var(--rule-soft)' }}>
                    <div style={{ fontSize: 12, fontWeight: 500 }}>{e.label}</div>
                    <div style={{ fontSize: 11, color: 'var(--ink-2)', marginTop: 2 }}>{e.value}</div>
                  </div>
                ))}
              </div>
              <div>
                <Mono upper size={9} color="var(--ink-3)" style={{ display: 'block', marginBottom: 8 }}>NEXT ACTIONS</Mono>
                {s.actions.map((a, i) => (
                  <div key={i} style={{ display: 'flex', gap: 10, padding: '6px 0', alignItems: 'flex-start' }}>
                    <Mono size={10} color="var(--ink-3)" style={{ width: 16, flexShrink: 0, paddingTop: 2 }}>0{i+1}</Mono>
                    <div style={{ fontSize: 12, color: 'var(--ink-2)', lineHeight: 1.55 }}>{a}</div>
                  </div>
                ))}
              </div>
            </div>
            <div style={{ display: 'flex', gap: 8, marginTop: 14 }}>
              <button className="ff-mono upper" style={{
                fontSize: 10, letterSpacing: '0.1em', padding: '8px 14px',
                background: 'var(--ink)', color: 'var(--bg)', border: 0, cursor: 'pointer',
              }}>Add to action plan ↗</button>
              <button className="ff-mono upper" style={{
                fontSize: 10, letterSpacing: '0.1em', padding: '8px 14px',
                background: 'transparent', color: 'var(--ink)', border: '1px solid var(--rule)', cursor: 'pointer',
              }}>Mark dismissed</button>
            </div>
          </div>
        )}
      </div>
    );
  }

  function FamilyFilter({ active, onChange, counts }) {
    const families = [
      { k: 'all',          l: 'All',          c: counts.all },
      { k: 'recovery',     l: 'Recovery',     c: counts.recovery || 0 },
      { k: 'collection',   l: 'Collection',   c: counts.collection || 0 },
      { k: 'distribution', l: 'Distribution', c: counts.distribution || 0 },
      { k: 'finance',      l: 'Finance',      c: counts.finance || 0 },
    ];
    return (
      <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
        {families.map(f => (
          <button key={f.k} onClick={() => onChange(f.k)} className="ff-mono upper" style={{
            fontSize: 10, letterSpacing: '0.08em', padding: '6px 10px',
            background: active === f.k ? 'var(--ink)' : 'transparent',
            color: active === f.k ? 'var(--bg)' : 'var(--ink-2)',
            border: '1px solid ' + (active === f.k ? 'var(--ink)' : 'var(--rule)'),
            cursor: 'pointer',
          }}>
            {f.l} <span style={{ opacity: 0.6 }}>{f.c}</span>
          </button>
        ))}
      </div>
    );
  }

  // ─── DRAWER ────────────────────────────────────────────────────
  function RoyaltyOptimizer() {
    const [open, setOpen] = _S(false);
    const [expanded, setExpanded] = _S(new Set([0]));  // top strategy expanded by default
    const [filter, setFilter] = _S('all');
    const [sort, setSort] = _S('impact');  // impact | effort | conf

    _E(() => {
      const onOpen = () => { setOpen(true); setExpanded(new Set([0])); };
      window.addEventListener('astro-open-royalty-optimizer', onOpen);
      return () => window.removeEventListener('astro-open-royalty-optimizer', onOpen);
    }, []);

    const result = _M(() => open ? analyze() : null, [open]);

    if (!open || !result) return null;

    const counts = { all: result.strategies.length };
    result.strategies.forEach(s => { counts[s.family] = (counts[s.family] || 0) + 1; });

    let visible = filter === 'all' ? result.strategies : result.strategies.filter(s => s.family === filter);
    visible = [...visible].sort((a, b) => {
      if (sort === 'impact') return b.annualLift - a.annualLift;
      if (sort === 'effort') return ({low:1,medium:2,high:3})[a.effort] - ({low:1,medium:2,high:3})[b.effort];
      if (sort === 'conf')   return b.conf - a.conf;
      return 0;
    });

    const toggle = (i) => {
      const next = new Set(expanded);
      if (next.has(i)) next.delete(i); else next.add(i);
      setExpanded(next);
    };

    return (
      <div style={{
        position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.45)', zIndex: 220,
        display: 'flex', justifyContent: 'flex-end',
      }} onClick={(e) => { if (e.target === e.currentTarget) setOpen(false); }}>
        <div style={{
          width: 1080, maxWidth: '96vw', height: '100vh', overflowY: 'auto',
          background: 'var(--paper)', boxShadow: '-8px 0 32px rgba(0,0,0,0.15)',
        }}>
          {/* Header */}
          <div style={{
            position: 'sticky', top: 0, zIndex: 2, background: 'var(--paper)',
            padding: '24px 32px 18px', borderBottom: '1px solid var(--rule)',
          }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 20 }}>
              <div>
                <Mono upper size={9} color="var(--ink-3)">ROYALTY OPTIMIZATION</Mono>
                <div className="ff-display" style={{ fontSize: 26, fontWeight: 600, letterSpacing: '-0.02em', marginTop: 4 }}>
                  {result.n} strategies · {fmtUsd(result.totalLift)} projected annual lift
                </div>
                <div style={{ fontSize: 12, color: 'var(--ink-2)', marginTop: 6, lineHeight: 1.5, maxWidth: 720 }}>
                  Ranked by impact-to-effort ratio. Top 3 alone represent {fmtUsd(result.topThree)} ({fmtPct(result.topThree / Math.max(1, result.totalLift))} of total opportunity)
                  against a {fmtUsd(result.grossAnnualized)} annualized baseline.
                </div>
              </div>
              <button onClick={() => setOpen(false)} style={{
                fontSize: 18, background: 'none', border: 0, cursor: 'pointer', color: 'var(--ink-3)', padding: 4, lineHeight: 1,
              }}>×</button>
            </div>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 16, marginTop: 18, flexWrap: 'wrap' }}>
              <FamilyFilter active={filter} onChange={setFilter} counts={counts}/>
              <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
                <Mono upper size={9} color="var(--ink-3)">SORT</Mono>
                {['impact','effort','conf'].map(k => (
                  <button key={k} onClick={() => setSort(k)} className="ff-mono upper" style={{
                    fontSize: 10, letterSpacing: '0.08em', padding: '5px 9px',
                    background: sort === k ? 'var(--ink)' : 'transparent',
                    color: sort === k ? 'var(--bg)' : 'var(--ink-2)',
                    border: '1px solid ' + (sort === k ? 'var(--ink)' : 'var(--rule)'),
                    cursor: 'pointer',
                  }}>{k}</button>
                ))}
              </div>
            </div>
          </div>

          {/* Headline impact strip */}
          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', borderBottom: '1px solid var(--rule)' }}>
            <ImpactCell label="TOTAL OPPORTUNITY" value={fmtUsd(result.totalLift)} sub={`${result.n} strategies`} />
            <ImpactCell label="TOP 3 IMPACT" value={fmtUsd(result.topThree)} sub={fmtPct(result.topThree / Math.max(1, result.totalLift)) + ' of total'} />
            <ImpactCell label="LOW-EFFORT QUICK WINS" value={fmtUsd(result.strategies.filter(s => s.effort === 'low').reduce((s,x) => s + x.annualLift, 0))} sub={`${result.strategies.filter(s => s.effort === 'low').length} actions · <30 days`} />
            <ImpactCell label="LIFT VS BASELINE" value={fmtPct(result.totalLift / Math.max(1, result.grossAnnualized))} sub={`baseline ${fmtUsd(result.grossAnnualized)}`} />
          </div>

          {/* Strategies */}
          <div style={{ padding: '22px 32px 40px' }}>
            {visible.map((s, i) => (
              <StrategyCard key={s.id} s={s} expanded={expanded.has(i)} onToggle={() => toggle(i)}/>
            ))}
            {visible.length === 0 && (
              <div style={{ padding: 40, textAlign: 'center', color: 'var(--ink-3)', fontSize: 13 }}>
                No strategies in this family.
              </div>
            )}
          </div>
        </div>
      </div>
    );
  }

  function ImpactCell({ label, value, sub }) {
    return (
      <div style={{ padding: '18px 24px', borderRight: '1px solid var(--rule)' }}>
        <Mono upper size={9} color="var(--ink-3)">{label}</Mono>
        <div className="ff-display" style={{ fontSize: 22, fontWeight: 600, letterSpacing: '-0.02em', marginTop: 4 }}>{value}</div>
        <div style={{ fontSize: 11, color: 'var(--ink-2)', marginTop: 3 }}>{sub}</div>
      </div>
    );
  }

  window.RoyaltyOptimizer = RoyaltyOptimizer;
  window.RoyaltyOptimizerEngine = { analyze };

  console.log('[RoyaltyOptimizer] loaded · 12 strategy families · listens for astro-open-royalty-optimizer');
})();
