/* global React, Ic, WORKS, RECORDINGS, RELEASES, AGREEMENTS, PROFILES, PUBLISHERS */
// inbox.jsx — Operating-loop Inbox.
//
// One front door for "what needs me today." Two tabs:
//   - TASKS    — actionable items with due dates (this file)
//   - MESSAGES — inbound notifications (delegates to ScreenNotifications)
//
// Tasks are *not* messages. Messages tell you something happened; tasks tell
// you to do something by a date. Some messages can become tasks (a CWR
// rejection becomes "fix and resend"); the inverse is rarely useful.
//
// Tasks intentionally stay narrow — payments due, agreements expiring,
// signatures pending, splits incomplete, MLC matches to confirm, releases to
// set up, black-box windows, conformance blockers, registration blockers,
// DSP anomalies that need a human. CWR rejections / claim notices live in
// Messages and convert in.
//
// Exports:
//   window.ScreenInbox  — the screen with both tabs
//   window.InboxStrip   — compact preview band for the Dashboard

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

  // ─────────────────────────── seeded RNG so the inbox is stable across reloads
  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;
  }

  // Today, frozen — matches workspace-screens.jsx + issues.jsx
  const NOW = new Date('2026-04-30T09:42:00');
  const daysFrom = (n) => { const d = new Date(NOW); d.setDate(d.getDate() + n); return d; };

  // ─────────────────────────── kinds (TASK kinds only; messages are separate)
  const KINDS = {
    payment_due:        { label: 'Payment due',          cat: 'money',      icon: 'Coin',   verb: 'Pay',           dest: 'royalties' },
    mlc_match:          { label: 'MLC match',            cat: 'money',      icon: 'Coin',   verb: 'Confirm match', dest: 'claims' },
    blackbox_urgent:    { label: 'Black-box closing',    cat: 'money',      icon: 'Coin',   verb: 'File claim',    dest: 'blackbox' },
    agreement_expiring: { label: 'Agreement expiring',   cat: 'rights',     icon: 'File',   verb: 'Review terms',  dest: 'agreement' },
    docuseal_pending:   { label: 'Awaiting signature',   cat: 'rights',     icon: 'File',   verb: 'Nudge signer',  dest: 'agreement' },
    splits_incomplete:  { label: 'Splits ≠ 100%',        cat: 'metadata',   icon: 'Disc',   verb: 'Fix splits',    dest: 'work' },
    registration_block: { label: 'Registration blocker', cat: 'compliance', icon: 'Shield', verb: 'Resolve',       dest: 'work' },
    anomaly_dsp:        { label: 'DSP anomaly',          cat: 'metadata',   icon: 'Bar',    verb: 'Investigate',   dest: 'analytics' },
    release_setup:      { label: 'Release setup',        cat: 'release',    icon: 'Disc',   verb: 'Open release',  dest: 'recording' },
    conformance_blocker:{ label: 'Conformance blocker',  cat: 'metadata',   icon: 'Build',  verb: 'Open run',      dest: 'conformance' },
    converted:          { label: 'From message',         cat: 'mixed',      icon: 'Send',   verb: 'Open',          dest: 'notifications' },
    promoted_issue:     { label: 'Promoted from Issues', cat: 'mixed',      icon: 'Shield', verb: 'Open issue',    dest: 'issues' },
    territory_conflict: { label: 'Territory conflict',   cat: 'rights',     icon: 'Shield', verb: 'Resolve overlap', dest: 'agreement' },
    localization_gap:   { label: 'Localization gap',     cat: 'metadata',   icon: 'Disc',   verb: 'Add translation', dest: 'work' },
    translation_returned:{label: 'Translation returned', cat: 'metadata',   icon: 'Disc',   verb: 'Review translation', dest: 'work' },
  };

  const SEVERITY = {
    blocker:   { label: 'BLOCKER',   rank: 0, color: 'var(--danger)' },
    overdue:   { label: 'OVERDUE',   rank: 1, color: 'var(--danger)' },
    today:     { label: 'TODAY',     rank: 2, color: 'var(--ink)' },
    this_week: { label: 'THIS WEEK', rank: 3, color: 'var(--ink-2)' },
    upcoming:  { label: 'UPCOMING',  rank: 4, color: 'var(--ink-3)' },
    fyi:       { label: 'FYI',       rank: 5, color: 'var(--ink-3)' },
  };

  const CATEGORIES = [
    { k: 'all',        label: 'All' },
    { k: 'money',      label: 'Money' },
    { k: 'rights',     label: 'Rights' },
    { k: 'compliance', label: 'Compliance' },
    { k: 'metadata',   label: 'Metadata' },
    { k: 'release',    label: 'Release' },
  ];

  // ─────────────────────────── deterministic task seed
  function buildTasks() {
    if (window.__INBOX_TASKS) return window.__INBOX_TASKS;
    const r = seed('astro·inbox·tasks·v1');
    const out = [];
    let id = 1;
    const push = (it) => out.push({
      id: `tsk_${String(id++).padStart(4, '0')}`,
      created_at: it.created_at || daysFrom(-Math.floor(r() * 30)),
      due_at: it.due_at || null,
      snoozed_until: null,
      status: 'open', // open | snoozed | done | dismissed
      ...it,
    });

    // 1) Payments due
    [
      { name: 'Jane Co-writer',     amount: 1_874.49,  label: 'Q1 2026 ASCAP performance',                  acct: 'ACH ····4422',  sev: 'today',   due: 0  },
      { name: 'The Other Side LLC', amount: 8_402.15,  label: 'TOS Pub-Admin Q1 collection',                acct: 'Wire · BoA',    sev: 'this_week', due: 5  },
      { name: 'M. Lee (producer)',  amount: 612.10,    label: 'Producer points · Late Bloomer (re-issue)', acct: 'ACH ····0091',  sev: 'this_week', due: 6  },
      { name: 'Solange Knowles',    amount: 22_104.88, label: 'Sub-pub Europe Q4 2025 — held pending W-8BEN', acct: 'pending',     sev: 'blocker', due: -2, blocker: 'W-8BEN missing' },
    ].forEach((p) => push({
      kind: 'payment_due',
      severity: p.sev,
      title: `Pay ${p.name} — $${p.amount.toLocaleString(undefined,{minimumFractionDigits:2,maximumFractionDigits:2})}`,
      summary: `${p.label} · ${p.acct}${p.blocker ? ' · ' + p.blocker : ''}`,
      meta: p,
      due_at: daysFrom(p.due),
    }));

    // 2) Agreements expiring (term timeline tasks)
    [
      { name: 'TOS Distribution 2023',         kind_l: 'Distribution', counterparty: 'The Other Side LLC',     daysOut: 21, sev: 'this_week' },
      { name: 'Symphonic — Rocket Science',    kind_l: 'Distribution', counterparty: 'Symphonic Distribution', daysOut: 58, sev: 'upcoming' },
      { name: 'Uroyan x RS Music Pub — Songwriter', kind_l: 'Songwriter',   counterparty: 'Paul Llanos',         daysOut: 84, sev: 'upcoming' },
    ].forEach((a) => push({
      kind: 'agreement_expiring',
      severity: a.sev,
      title: a.name,
      summary: `${a.kind_l} · expires in ${a.daysOut}d · counterparty ${a.counterparty} · notice window opens at T-30`,
      meta: a,
      due_at: daysFrom(Math.max(0, a.daysOut - 30)),
    }));

    // 3) Docuseal envelopes pending signature
    push({
      kind: 'docuseal_pending', severity: 'today',
      title: 'TOS Sync License v3 — awaiting Caro Mendoza',
      summary: 'Sent 6d ago · 2 of 3 signers complete · viewed yesterday · expires in 8d',
      meta: { envelope: 'env_8421', signed: 2, total: 3, viewed: true },
      due_at: daysFrom(2),
      created_at: daysFrom(-6),
    });
    push({
      kind: 'docuseal_pending', severity: 'this_week',
      title: 'Producer agreement — M. Lee — Late Bloomer (re-issue)',
      summary: 'Sent 11d ago · not yet viewed · expires in 19d',
      meta: { envelope: 'env_8430', signed: 0, total: 2, viewed: false },
      due_at: daysFrom(19),
      created_at: daysFrom(-11),
    });

    // 4) Registration blockers (gate CWR queue)
    push({
      kind: 'registration_block', severity: 'blocker',
      title: 'CWR queue · 4 works missing IPI on writer',
      summary: 'Cannot register until IPI is supplied for: T. Aguilar, R. Wynne, J. Ortiz, M. Casas',
      meta: { count: 4 },
      due_at: daysFrom(0),
      created_at: daysFrom(-4),
    });

    // 5) Splits ≠ 100% (action — fix metadata)
    [
      { title: 'Late Bloomer',          iswc: 'T-998.221.001-4', off: -10, sev: 'today', due: 0 },
      { title: 'Eres Tú · acoustic',    iswc: 'T-998.221.018-7', off: 5,   sev: 'this_week', due: 4 },
      { title: 'Veranda 4AM',           iswc: 'T-998.221.071-2', off: -15, sev: 'this_week', due: 5 },
    ].forEach((w) => push({
      kind: 'splits_incomplete',
      severity: w.sev,
      title: `${w.title} — splits ${w.off > 0 ? '+' : ''}${w.off}%`,
      summary: `${w.iswc} · current total ${100 + w.off}% · ${w.off > 0 ? 'reduce' : 'add'} share to balance to 100%`,
      meta: w,
      due_at: daysFrom(w.due),
    }));

    // 6) MLC matches to confirm
    push({
      kind: 'mlc_match', severity: 'this_week',
      title: 'MLC match · 47 works to confirm',
      summary: '47 candidate matches at ≥92% confidence · $14,202 hold pending confirmation',
      meta: { count: 47, hold: 14_202 },
      due_at: daysFrom(6),
      created_at: daysFrom(-2),
    });
    push({
      kind: 'mlc_match', severity: 'upcoming',
      title: 'MLC dispute · "Eres Tú" — Latin Tracks Inc claims 50%',
      summary: 'Counterclaim filed 4d ago · response due in 21d',
      meta: { case: 'MLC-DSP-2026-0091' },
      due_at: daysFrom(21),
      created_at: daysFrom(-4),
    });

    // 7) Releases — new managed-artist releases needing setup
    push({
      kind: 'release_setup', severity: 'this_week',
      title: 'New release · Solange Knowles — "Holy Grain (single)" — 2026-05-22',
      summary: 'Distributor delivery built · DSP profile sync pending · social copy not drafted',
      meta: { artist: 'Solange Knowles', date: '2026-05-22', upc: '602448923741' },
      due_at: daysFrom(5),
      created_at: daysFrom(-1),
    });
    push({
      kind: 'release_setup', severity: 'upcoming',
      title: 'New release · Uroyan — "Late Bloomer (live at Rough Trade)" — 2026-06-14',
      summary: 'Master delivered · artwork v2 in review · ISRC pending · CWR queued',
      meta: { artist: 'Uroyan', date: '2026-06-14', upc: '602448923748' },
      due_at: daysFrom(14),
      created_at: daysFrom(-3),
    });

    // 8) DSP anomalies needing eyes (analytical task — not a message)
    push({
      kind: 'anomaly_dsp', severity: 'today',
      title: 'Spotify · "Late Bloomer" daily streams +412% vs 30-day avg',
      summary: '14,902 streams yesterday · normal range 1,200–3,800 · likely playlist add — verify',
      meta: { dsp: 'Spotify', track: 'Late Bloomer', delta: '+412%' },
      due_at: daysFrom(0),
      created_at: daysFrom(0),
    });
    push({
      kind: 'anomaly_dsp', severity: 'this_week',
      title: 'Apple Music · "Eres Tú" dropped from "Acústicos Esenciales"',
      summary: 'Removed 3d ago · -68% daily streams since · pitch a replacement?',
      meta: { dsp: 'Apple Music', track: 'Eres Tú' },
      due_at: daysFrom(4),
      created_at: daysFrom(-3),
    });

    // 9) Black-box closing
    push({
      kind: 'blackbox_urgent', severity: 'overdue',
      title: 'GEMA Q3 2025 pool — distribution window closes in 4d',
      summary: '$28K unallocated · 142 candidate lines · file claims now',
      meta: { society: 'GEMA', pool: 'GEMA-Q3-2025', amount: 28_000 },
      due_at: daysFrom(4),
      created_at: daysFrom(-22),
    });

    // 10a) Territory conflicts (surfaced from TerritoryEngine.findConflicts)
    push({
      kind: 'territory_conflict', severity: 'blocker',
      title: 'Territory overlap · TOS Sub-Pub EU 2024 ↔ Symphonic Distribution',
      summary: 'Both deals claim mechanical rights in DE, FR, NL, BE, IT, ES (6 ISO) for 2024-Q3 — block statement until reconciled.',
      meta: { agA: 'AGR-0142', agB: 'AGR-0188', isos: ['DE','FR','NL','BE','IT','ES'], rights: ['mech'] },
      due_at: daysFrom(0),
      created_at: daysFrom(-1),
    });
    push({
      kind: 'territory_conflict', severity: 'this_week',
      title: 'Territory overlap · Uroyan Songwriter ↔ Paul Llanos Co-Pub',
      summary: 'Performance rights overlap in MX, CO, AR, CL (4 ISO) for 2025 term — review before next CWR submission.',
      meta: { agA: 'AGR-0203', agB: 'AGR-0211', isos: ['MX','CO','AR','CL'], rights: ['perf'] },
      due_at: daysFrom(5),
      created_at: daysFrom(-2),
    });

    // 10b) Localization gaps (top-$ leakage from LocalizationEngine.findGaps)
    [
      { work: 'Late Bloomer',     iswc: 'T-998.221.001-4', market: 'KR', script: 'Hangul',         loss: 4_812, sev: 'today',     due: 1 },
      { work: 'Eres Tú',          iswc: 'T-998.221.018-7', market: 'JP', script: 'Katakana',       loss: 2_104, sev: 'this_week', due: 6 },
      { work: 'Veranda 4AM',      iswc: 'T-998.221.071-2', market: 'CN', script: 'Simp. Chinese',  loss: 1_580, sev: 'this_week', due: 6 },
    ].forEach(g => push({
      kind: 'localization_gap',
      severity: g.sev,
      title: `Add ${g.market} title — ${g.work}`,
      summary: `${g.iswc} · ${g.script} script missing · est $${g.loss.toLocaleString()}/yr leakage from ${g.market} society payouts`,
      meta: g,
      due_at: daysFrom(g.due),
    }));

    // 10c) Translations returned from outsourced queue — needs review
    push({
      kind: 'translation_returned', severity: 'today',
      title: 'KR translation returned · "Holy Grain" — Solange Knowles',
      summary: 'Translator: Yun-Seo Park · returned 4h ago · romanized + Hangul + lyric adaptation · approve to register with KOMCA',
      meta: { work: 'Holy Grain', market: 'KR', vendor: 'Yun-Seo Park' },
      due_at: daysFrom(1),
      created_at: daysFrom(0),
    });

    // 10) Conformance blocker
    push({
      kind: 'conformance_blocker', severity: 'blocker',
      title: 'Last conformance run — 12 blockers unresolved',
      summary: 'Run #3 · 2026-04-28 · 12 agreements without parties · cannot commit migration',
      meta: { run: 3 },
      due_at: daysFrom(0),
      created_at: daysFrom(-2),
    });

    window.__INBOX_TASKS = out;
    return out;
  }

  // Counts (for badge / preview strip / rail).
  function refreshCounts() {
    const tasks = buildTasks();
    const open = tasks.filter(t => t.status === 'open');
    window.__INBOX_OPEN_COUNT = open.length;
    window.__INBOX_BLOCKERS = open.filter(t => t.severity === 'blocker' || t.severity === 'overdue').length;
    window.__INBOX_TODAY = open.filter(t => t.severity === 'today' || t.severity === 'overdue' || t.severity === 'blocker').length;
    // Combined inbox badge (tasks today + unread messages)
    const unread = window.__NOTIFICATIONS_UNREAD || 0;
    window.__INBOX_TOTAL_BADGE = window.__INBOX_TODAY + unread;
    return { tasks, open };
  }
  refreshCounts();
  window.__INBOX_REFRESH = refreshCounts;

  // Promote an Issues anomaly into an inbox task. Idempotent on anomaly_id —
  // re-promoting the same anomaly returns the existing task instead of duplicating.
  // Returns the task object so callers can stash its id on the source anomaly.
  window.__INBOX_PROMOTE = function promoteAnomaly(anom, opts) {
    if (!anom || !anom.id) return null;
    const tasks = buildTasks();
    const existing = tasks.find(t => t.kind === 'promoted_issue' && t.meta && t.meta.anomaly_id === anom.id);
    if (existing) return existing;

    // Map anomaly severity → task severity
    const sevMap = { critical: 'blocker', high: 'today', medium: 'this_week', low: 'upcoming' };
    const taskSev = (opts && opts.severity) || sevMap[anom.severity] || 'this_week';
    // Due date: critical → today, high → +2d, else +7d
    const dueOffset = taskSev === 'blocker' ? 0 : taskSev === 'today' ? 2 : taskSev === 'this_week' ? 7 : 14;

    const id = `tsk_pro_${anom.id}`;
    const task = {
      id,
      kind: 'promoted_issue',
      severity: taskSev,
      title: (anom.anomaly_meta && anom.anomaly_meta.label ? anom.anomaly_meta.label + ' · ' : '') + (anom.entity_label || anom.report_id),
      summary: anom.description || '',
      meta: {
        anomaly_id: anom.id,
        report_id: anom.report_id,
        anomaly_type: anom.anomaly_type,
        category: anom.anomaly_meta && anom.anomaly_meta.cat,
        entity_type: anom.entity_type,
        entity_id: anom.entity_id,
      },
      created_at: new Date(),
      due_at: daysFrom(dueOffset),
      snoozed_until: null,
      status: 'open',
    };
    tasks.unshift(task); // newest first; sort still applies via severity
    window.__INBOX_TASKS = tasks;
    refreshCounts();
    return task;
  };

  // Seed: pre-promote a couple of high-severity anomalies so the bridge is
  // discoverable on first load. Picks from the issues cache when available.
  (function seedPromoted() {
    const cache = window.__ANOMALIES_CACHE || [];
    if (!cache.length) return;
    // Two deterministic picks: one critical, one CWR rejection.
    const targets = [
      cache.find(a => a.severity === 'critical' && a.status === 'open'),
      cache.find(a => a.anomaly_type === 'cwr_rejection' && a.status === 'open'),
    ].filter(Boolean);
    targets.forEach(a => {
      const t = window.__INBOX_PROMOTE(a);
      if (t) a._promoted_task_id = t.id;
    });
  })();

  // ─────────────────────────── helpers
  const fmtRel = (d) => {
    if (!d) return '';
    const dd = Math.round((d - NOW) / 86400000);
    if (dd === 0) return 'today';
    if (dd === 1) return 'tomorrow';
    if (dd === -1) return 'yesterday';
    if (dd > 0 && dd <= 14) return `in ${dd}d`;
    if (dd < 0 && dd >= -14) return `${-dd}d ago`;
    return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
  };
  const fmtAge = (d) => fmtRel(d).replace('today', 'just now');

  // ─────────────────────────── TASK ROW
  function TaskRow({ task, onAction, onSnooze, onDone }) {
    const k = KINDS[task.kind] || {};
    const sev = SEVERITY[task.severity] || SEVERITY.fyi;
    const Icon = (window.Ic && window.Ic[k.icon]) || (window.Ic && window.Ic.Disc);
    const verb = k.verb || 'Open';

    return (
      <div
        onClick={() => onAction(task)}
        style={{
          display: 'grid',
          gridTemplateColumns: '34px 1fr 200px 200px',
          gap: 14,
          alignItems: 'center',
          padding: '14px 18px',
          borderBottom: '1px solid var(--rule-soft)',
          cursor: 'pointer',
          background: 'var(--bg)',
          transition: 'background .12s',
        }}
        onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bg-2)'}
        onMouseLeave={(e) => e.currentTarget.style.background = 'var(--bg)'}
      >
        <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          <span style={{ width: 4, height: 28, background: sev.color, flexShrink: 0 }} />
          {Icon ? <Icon width={16} height={16} style={{ color: 'var(--ink-3)' }} /> : null}
        </div>

        <div style={{ minWidth: 0 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 3 }}>
            <span className="ff-mono upper" style={{
              fontSize: 9, letterSpacing: '.1em', color: sev.color,
              padding: '2px 5px', border: `1px solid ${sev.color}`, flexShrink: 0,
            }}>{sev.label}</span>
            <span className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.1em', color: 'var(--ink-3)', flexShrink: 0 }}>
              {k.label || task.kind}
            </span>
            <span style={{
              fontSize: 14, fontWeight: 600, color: 'var(--ink)',
              overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
              minWidth: 0, flex: 1,
            }}>{task.title}</span>
          </div>
          <div style={{ fontSize: 12, color: 'var(--ink-3)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
            {task.summary}
          </div>
        </div>

        <div style={{ textAlign: 'right' }}>
          <div className="ff-mono num upper" style={{
            fontSize: 11, fontWeight: 600, letterSpacing: '.06em',
            color: (task.severity === 'overdue' || task.severity === 'blocker') ? 'var(--danger)' : 'var(--ink-2)',
          }}>
            DUE {fmtRel(task.due_at).toUpperCase()}
          </div>
          <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 3 }}>
            opened {fmtAge(task.created_at)}
          </div>
        </div>

        <div style={{ display: 'flex', gap: 6, justifyContent: 'flex-end' }} onClick={(e) => e.stopPropagation()}>
          <button onClick={() => onAction(task)} className="ff-mono upper" style={{
            fontSize: 10, letterSpacing: '.08em', padding: '7px 10px',
            background: 'var(--ink)', color: 'var(--bg)', border: 0, cursor: 'pointer',
          }}>{verb} →</button>
          <button onClick={() => onSnooze(task)} title="Snooze 7 days" className="ff-mono upper" style={{
            fontSize: 10, letterSpacing: '.08em', padding: '7px 8px',
            background: 'transparent', color: 'var(--ink-3)', border: '1px solid var(--rule)', cursor: 'pointer',
          }}>snooze</button>
          <button onClick={() => onDone(task)} title="Mark done" className="ff-mono upper" style={{
            fontSize: 10, letterSpacing: '.08em', padding: '7px 8px',
            background: 'transparent', color: 'var(--ink-3)', border: '1px solid var(--rule)', cursor: 'pointer',
          }}>done</button>
        </div>
      </div>
    );
  }

  // ─────────────────────────── TASKS PANE
  function TasksPane({ go }) {
    const [tasks, setTasks] = useState(() => buildTasks());
    const [filter, setFilter] = useState('now'); // now | week | all | snoozed | done
    const [cat, setCat] = useState('all');
    const [q, setQ] = useState('');

    const update = (id, patch) => {
      setTasks(prev => {
        const next = prev.map(t => t.id === id ? { ...t, ...patch } : t);
        window.__INBOX_TASKS = next;
        refreshCounts();
        return next;
      });
    };

    const filtered = useMemo(() => {
      let xs = tasks.slice();
      if (filter === 'now')      xs = xs.filter(t => t.status === 'open' && (t.severity === 'blocker' || t.severity === 'overdue' || t.severity === 'today'));
      else if (filter === 'week') xs = xs.filter(t => t.status === 'open' && (t.severity === 'this_week' || t.severity === 'today' || t.severity === 'overdue' || t.severity === 'blocker'));
      else if (filter === 'snoozed') xs = xs.filter(t => t.status === 'snoozed');
      else if (filter === 'done')    xs = xs.filter(t => t.status === 'done' || t.status === 'dismissed');
      else                           xs = xs.filter(t => t.status === 'open');

      if (cat !== 'all') xs = xs.filter(t => (KINDS[t.kind]?.cat) === cat);
      if (q.trim()) {
        const ql = q.toLowerCase();
        xs = xs.filter(t =>
          (t.title || '').toLowerCase().includes(ql) ||
          (t.summary || '').toLowerCase().includes(ql) ||
          (KINDS[t.kind]?.label || '').toLowerCase().includes(ql)
        );
      }
      xs.sort((a, b) => {
        const ra = (SEVERITY[a.severity] || SEVERITY.fyi).rank;
        const rb = (SEVERITY[b.severity] || SEVERITY.fyi).rank;
        if (ra !== rb) return ra - rb;
        const da = a.due_at ? +a.due_at : Infinity;
        const db = b.due_at ? +b.due_at : Infinity;
        return da - db;
      });
      return xs;
    }, [tasks, filter, cat, q]);

    const counts = useMemo(() => {
      const open = tasks.filter(t => t.status === 'open');
      return {
        now: open.filter(t => t.severity === 'blocker' || t.severity === 'overdue' || t.severity === 'today').length,
        week: open.filter(t => t.severity === 'this_week' || t.severity === 'today' || t.severity === 'overdue' || t.severity === 'blocker').length,
        all: open.length,
        snoozed: tasks.filter(t => t.status === 'snoozed').length,
        done: tasks.filter(t => t.status === 'done' || t.status === 'dismissed').length,
      };
    }, [tasks]);

    const onAction = (t) => {
      const dest = KINDS[t.kind]?.dest;
      if (!dest) return;
      if (t.kind === 'splits_incomplete' || t.kind === 'registration_block') go('catalog', { tab: 'songs' });
      else if (t.kind === 'docuseal_pending' || t.kind === 'agreement_expiring') go('agreement', { id: t.meta?.envelope || t.title });
      else if (t.kind === 'release_setup') go('catalog', { tab: 'releases' });
      else if (t.kind === 'conformance_blocker') go('conformance');
      else if (t.kind === 'promoted_issue') go('issues', { open_anomaly_id: t.meta?.anomaly_id });
      else if (t.kind === 'territory_conflict') go('agreement', { id: t.meta?.agA });
      else if (t.kind === 'localization_gap' || t.kind === 'translation_returned') go('catalog', { tab: 'songs' });
      else go(dest);
    };
    const onSnooze = (t) => update(t.id, { status: 'snoozed', snoozed_until: daysFrom(7) });
    const onDone = (t) => update(t.id, { status: 'done' });

    const tabs = [
      { k: 'now',     label: 'Now',       n: counts.now,     desc: 'Blockers · overdue · today' },
      { k: 'week',    label: 'This week', n: counts.week,    desc: 'Now + next 7 days' },
      { k: 'all',     label: 'All open',  n: counts.all,     desc: 'Everything open' },
      { k: 'snoozed', label: 'Snoozed',   n: counts.snoozed, desc: 'Hidden until later' },
      { k: 'done',    label: 'Done',      n: counts.done,    desc: 'Resolved this month' },
    ];

    return (
      <div>
        {/* At-a-glance severity strip */}
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4,1fr)', borderTop: '1px solid var(--rule)', borderBottom: '1px solid var(--rule)', marginBottom: 0 }}>
          {[
            { l: 'BLOCKERS',  n: tasks.filter(t => t.status === 'open' && t.severity === 'blocker').length,   c: 'var(--danger)', sub: 'Cannot proceed' },
            { l: 'OVERDUE',   n: tasks.filter(t => t.status === 'open' && t.severity === 'overdue').length,   c: 'var(--danger)', sub: 'Past due date' },
            { l: 'TODAY',     n: tasks.filter(t => t.status === 'open' && t.severity === 'today').length,     c: 'var(--ink)',    sub: 'Due today' },
            { l: 'THIS WEEK', n: tasks.filter(t => t.status === 'open' && t.severity === 'this_week').length, c: 'var(--ink-2)',  sub: 'Due in 7 days' },
          ].map((s, i, arr) => (
            <div key={s.l} style={{ padding: '20px 22px', borderRight: i === arr.length - 1 ? 'none' : '1px solid var(--rule)' }}>
              <div className="ff-mono upper" style={{ fontSize: 10, letterSpacing: '.12em', color: 'var(--ink-3)' }}>{s.l}</div>
              <div className="ff-display num" style={{ fontSize: 44, fontWeight: 700, letterSpacing: '-0.04em', lineHeight: 1, color: s.c, marginTop: 4 }}>{s.n}</div>
              <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 4 }}>{s.sub}</div>
            </div>
          ))}
        </div>

        {/* Tabs + filter */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 0, borderBottom: '1px solid var(--rule)' }}>
          {tabs.map(t => (
            <button key={t.k} onClick={() => setFilter(t.k)} style={{
              padding: '14px 18px',
              borderBottom: filter === t.k ? '2px solid var(--ink)' : '2px solid transparent',
              marginBottom: -1, cursor: 'pointer', background: 'transparent',
              color: filter === t.k ? 'var(--ink)' : 'var(--ink-3)',
            }}>
              <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
                <span className="ff-display" style={{ fontSize: 16, fontWeight: 600, letterSpacing: '-0.02em' }}>{t.label}</span>
                <span className="ff-mono num" style={{ fontSize: 11, color: filter === t.k ? 'var(--ink-2)' : 'var(--ink-3)', fontWeight: 500 }}>{t.n}</span>
              </div>
              <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 3, textAlign: 'left' }}>{t.desc}</div>
            </button>
          ))}
          <span style={{ flex: 1 }} />
          <input
            value={q} onChange={(e) => setQ(e.target.value)} placeholder="Filter…"
            className="ff-mono"
            style={{ fontSize: 12, padding: '7px 10px', border: '1px solid var(--rule)',
              background: 'var(--paper)', color: 'var(--ink)', minWidth: 200, marginRight: 14 }}
          />
        </div>

        {/* Category chips */}
        <div style={{ display: 'flex', gap: 6, padding: '14px 0', flexWrap: 'wrap' }}>
          {CATEGORIES.map(c => (
            <button key={c.k} onClick={() => setCat(c.k)} className="ff-mono upper" style={{
              fontSize: 10, letterSpacing: '.08em', padding: '6px 10px',
              border: '1px solid ' + (cat === c.k ? 'var(--ink)' : 'var(--rule)'),
              background: cat === c.k ? 'var(--ink)' : 'transparent',
              color: cat === c.k ? 'var(--bg)' : 'var(--ink-2)', cursor: 'pointer',
            }}>{c.label}</button>
          ))}
        </div>

        {/* List */}
        <div style={{ border: '1px solid var(--rule)', background: 'var(--bg)' }}>
          {filtered.length === 0 ? (
            <div style={{ padding: '64px 24px', textAlign: 'center' }}>
              <div className="ff-display" style={{ fontSize: 32, fontWeight: 600, letterSpacing: '-0.03em', marginBottom: 8 }}>Inbox zero.</div>
              <div className="ff-mono upper" style={{ fontSize: 11, letterSpacing: '.1em', color: 'var(--ink-3)' }}>
                {filter === 'now' ? 'Nothing urgent — go make something.' : 'No tasks match the current filter.'}
              </div>
            </div>
          ) : (
            filtered.map(t => (
              <TaskRow key={t.id} task={t}
                onAction={onAction} onSnooze={onSnooze} onDone={onDone} />
            ))
          )}
        </div>

        <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 14, textAlign: 'right' }}>
          {filtered.length} tasks · sorted by severity, then due date · snoozes return after 7 days
        </div>
      </div>
    );
  }

  // ─────────────────────────── MAIN SCREEN — Tasks + Messages
  function ScreenInbox({ go, payload }) {
    const initialTab = (payload && payload.tab) || 'tasks';
    const [tab, setTab] = useState(initialTab);

    // Re-read counts when switching tabs (Notifications mutates them).
    const [tick, setTick] = useState(0);
    useEffect(() => {
      const onMsg = () => setTick(x => x + 1);
      window.addEventListener('astro-route', onMsg);
      return () => window.removeEventListener('astro-route', onMsg);
    }, []);

    const taskCount = (window.__INBOX_TODAY || 0);
    const msgUnread = (window.__NOTIFICATIONS_UNREAD || 0);

    return (
      <div>
        {/* Header */}
        <div style={{ borderBottom: '1px solid var(--rule)', paddingBottom: 24, marginBottom: 24 }}>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 14, marginBottom: 12 }}>
            <span className="ff-mono upper" style={{ fontSize: 11, color: 'var(--ink-3)', letterSpacing: '.14em' }}>02 — INBOX</span>
            <span style={{ flex: 1, height: 1, background: 'var(--rule)' }} />
            <span className="ff-mono num upper" style={{ fontSize: 11, color: 'var(--ink-3)' }}>
              {NOW.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' }).toUpperCase()}
            </span>
          </div>

          <h1 className="heading-swap ff-display" style={{
            fontSize: 'clamp(48px, 6.5vw, 96px)', fontWeight: 700, letterSpacing: '-0.045em',
            lineHeight: 1, marginBottom: 18,
          }}>
            What needs you today.
          </h1>

          {/* Tasks / Messages tab strip */}
          <div style={{ display: 'flex', gap: 0, borderBottom: '1px solid var(--rule)', marginTop: 8 }}>
            {[
              { k: 'tasks',    label: 'Tasks',    n: taskCount, sub: 'What to do · with due dates' },
              { k: 'messages', label: 'Messages', n: msgUnread, sub: 'What happened · from societies, DSPs, teammates' },
            ].map(t => {
              const active = tab === t.k;
              return (
                <button key={t.k} onClick={() => setTab(t.k)} style={{
                  padding: '16px 22px', cursor: 'pointer', background: 'transparent',
                  borderBottom: active ? '3px solid var(--ink)' : '3px solid transparent',
                  marginBottom: -1, color: active ? 'var(--ink)' : 'var(--ink-3)',
                  textAlign: 'left',
                }}>
                  <div style={{ display: 'flex', alignItems: 'baseline', gap: 10 }}>
                    <span className="ff-display" style={{ fontSize: 22, fontWeight: 600, letterSpacing: '-0.02em' }}>{t.label}</span>
                    {t.n > 0 && (
                      <span className="ff-mono num" style={{
                        fontSize: 11, padding: '3px 7px', fontWeight: 600,
                        background: active ? 'var(--ink)' : 'var(--bg-2)',
                        color: active ? 'var(--bg)' : 'var(--ink-2)',
                        border: '1px solid ' + (active ? 'var(--ink)' : 'var(--rule)'),
                      }}>{t.n}</span>
                    )}
                  </div>
                  <div className="ff-mono" style={{ fontSize: 10, color: 'var(--ink-3)', marginTop: 4, letterSpacing: '.04em' }}>{t.sub}</div>
                </button>
              );
            })}
          </div>
        </div>

        {/* Pane */}
        {tab === 'tasks' ? (
          <TasksPane go={go} />
        ) : (
          window.ScreenNotifications
            ? <window.ScreenNotifications go={go} />
            : <div className="ff-mono" style={{ padding: 40, color: 'var(--ink-3)' }}>Messages unavailable.</div>
        )}
      </div>
    );
  }

  // ─────────────────────────── DASHBOARD STRIP
  function InboxStrip({ go }) {
    const tasks = buildTasks();
    const open = tasks.filter(t => t.status === 'open');
    const now = open
      .filter(t => t.severity === 'blocker' || t.severity === 'overdue' || t.severity === 'today')
      .sort((a, b) => (SEVERITY[a.severity].rank - SEVERITY[b.severity].rank));
    const top = now.slice(0, 3);

    const blockers = open.filter(t => t.severity === 'blocker').length;
    const overdue  = open.filter(t => t.severity === 'overdue').length;
    const unread = window.__NOTIFICATIONS_UNREAD || 0;

    return (
      <div style={{ border: '1px solid var(--rule)', background: 'var(--bg)', marginBottom: 32 }}>
        <div style={{
          display: 'flex', alignItems: 'center', gap: 14,
          padding: '14px 18px', borderBottom: top.length ? '1px solid var(--rule-soft)' : 'none',
          background: 'var(--bg-2)',
        }}>
          <span className="ff-mono upper" style={{ fontSize: 10, letterSpacing: '.14em', color: 'var(--ink-3)' }}>INBOX</span>
          <span className="ff-display num" style={{ fontSize: 22, fontWeight: 700, letterSpacing: '-0.03em' }}>{now.length}</span>
          <span className="ff-mono upper" style={{ fontSize: 10, letterSpacing: '.1em', color: 'var(--ink-3)' }}>NEED YOU TODAY</span>
          <span style={{ flex: 1 }} />
          <span className="ff-mono num" style={{ fontSize: 11, color: 'var(--ink-3)' }}>
            {blockers} blockers · {overdue} overdue · {open.length} open · {unread} unread messages
          </span>
          <button onClick={() => go('inbox')} className="ff-mono upper" style={{
            fontSize: 10, letterSpacing: '.08em', padding: '6px 10px',
            border: '1px solid var(--ink)', background: 'var(--ink)', color: 'var(--bg)', cursor: 'pointer',
          }}>Open inbox →</button>
        </div>
        {top.map(t => {
          const k = KINDS[t.kind] || {};
          const sev = SEVERITY[t.severity] || SEVERITY.fyi;
          return (
            <div key={t.id} onClick={() => go('inbox')} style={{
              display: 'grid', gridTemplateColumns: '4px 1fr 140px',
              gap: 12, alignItems: 'center',
              padding: '10px 18px', borderBottom: '1px solid var(--rule-soft)', cursor: 'pointer',
            }}
              onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bg-2)'}
              onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}
            >
              <span style={{ width: 4, height: 22, background: sev.color }} />
              <div style={{ minWidth: 0 }}>
                <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
                  <span className="ff-mono upper" style={{ fontSize: 9, letterSpacing: '.1em', color: sev.color }}>{sev.label}</span>
                  <span style={{ fontSize: 13, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{t.title}</span>
                </div>
                <div style={{ fontSize: 11, color: 'var(--ink-3)', marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                  {k.label} · {t.summary}
                </div>
              </div>
              <span className="ff-mono upper" style={{ fontSize: 10, letterSpacing: '.08em', color: 'var(--ink-3)', textAlign: 'right' }}>
                {(k.verb || 'Open')} →
              </span>
            </div>
          );
        })}
      </div>
    );
  }

  // Export
  window.ScreenInbox = ScreenInbox;
  window.InboxStrip = InboxStrip;
})();
