// TodayWorkspace — surfaces the daily OpenClaw cascade output that's
// otherwise invisible without curl: the morning brief, each persona's
// same-day memos (PhD Researcher's pre-market scan, Hedging Specialist's
// risk addendum), and the append-only status log.
//
// Replaces email as the primary morning surface (Gmail App Password is
// dead until rotated; daily cascade still runs and lands on disk —
// this tab makes it visible).
//
// Hits GET /api/today (powered by collect_today_summary in
// backend/agent/persona_runner.py). Auto-polls every 60s so a memo that
// the cron just filed shows up without manual refresh.

const { useState: tUseState, useEffect: tUseEffect, useCallback: tUseCallback, useMemo: tUseMemo } = React;

const TODAY_POLL_MS = 60_000;

function todayDateISO() {
  // Local date is fine for the picker — backend interprets as UTC date,
  // and the cascade is timezone-tagged in cron_tz anyway.
  const d = new Date();
  const y = d.getFullYear();
  const m = String(d.getMonth() + 1).padStart(2, '0');
  const dd = String(d.getDate()).padStart(2, '0');
  return `${y}-${m}-${dd}`;
}

function fmtRelTime(isoStr) {
  if (!isoStr) return '';
  try {
    const t = new Date(isoStr);
    const diffMin = Math.floor((Date.now() - t.getTime()) / 60000);
    if (diffMin < 1) return 'just now';
    if (diffMin < 60) return `${diffMin}m ago`;
    const h = Math.floor(diffMin / 60);
    if (h < 24) return `${h}h ago`;
    return `${Math.floor(h / 24)}d ago`;
  } catch { return ''; }
}

function outcomeColor(outcome) {
  if (outcome === 'ok')      return '#5fb37c';   // green
  if (outcome === 'fail')    return '#d96a6a';   // red
  if (outcome === 'partial') return '#d9a85f';   // amber
  if (outcome === 'skipped') return 'var(--text-dim)';
  if (outcome === 'started') return '#d9a85f';
  return 'var(--text-dim)';
}

// Render markdown safely-ish. We trust our own brief output (composed by
// our own LLM call into our own vault), so dangerouslySetInnerHTML on
// marked output is fine here — we control both ends. If you ever pipe
// untrusted content here, swap in DOMPurify.
function MarkdownBlock({ source }) {
  const html = tUseMemo(() => {
    if (!source) return '';
    try { return window.marked.parse(source); }
    catch { return `<pre>${(source || '').replace(/[<>&]/g, c => ({ '<':'&lt;','>':'&gt;','&':'&amp;'}[c]))}</pre>`; }
  }, [source]);
  return <div className="today-md" dangerouslySetInnerHTML={{ __html: html }} />;
}

// Phase 6 R · #3 — Overnight gap strip. The 06:00 ET first-glance:
// "where are S&P futures, what's VIX doing, and did anything in my
// book gap?" Pulled from window.getQuote (Massive Market Data lib —
// pre-market quotes). Auto-refreshes on tab focus; doesn't poll
// aggressively (1 fetch on mount, 1 on focus = at most a few per
// hour while tab is visible).
//
// Macro symbols: SPY (S&P proxy for futures), QQQ (NQ), VIX,
//                TLT (10y duration), GLD (gold), DXY (USD)
// Per-position symbols: pulled from /api/options/positions
function OvernightStrip() {
  const [quotes, setQuotes] = tUseState({});
  const [positions, setPositions] = tUseState([]);
  const MACRO = ['SPY', 'QQQ', 'VIX', 'TLT', 'GLD'];
  tUseEffect(() => {
    let cancel = false;
    const load = async () => {
      try {
        // 1. fetch open positions for the book
        const r = await fetch('/api/options/positions');
        if (r.ok) {
          const j = await r.json();
          if (!cancel) {
            const tickers = [...new Set((j.positions || []).map(p => p.ticker).filter(Boolean))];
            setPositions(tickers);
          }
        }
        // 2. fetch quotes for macro + position tickers
        if (typeof window.getQuote !== 'function') return;
        const symbols = [...MACRO, ...(positions || [])];
        const uniqSyms = [...new Set(symbols)];
        const results = await Promise.all(uniqSyms.map(async sym => {
          try { return [sym, await window.getQuote(sym)]; }
          catch { return [sym, null]; }
        }));
        if (!cancel) {
          const next = {};
          for (const [sym, q] of results) if (q) next[sym] = q;
          setQuotes(next);
        }
      } catch {}
    };
    load();
    const onFocus = () => { if (document.visibilityState === 'visible') load(); };
    document.addEventListener('visibilitychange', onFocus);
    return () => { cancel = true; document.removeEventListener('visibilitychange', onFocus); };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  const macroQuotes = MACRO.map(s => ({ symbol: s, q: quotes[s] }));
  // Gap movers in book: |change%| > 1.5
  const gapMovers = positions
    .filter(t => quotes[t] && Math.abs(quotes[t].changePct || 0) > 1.5)
    .map(t => ({ symbol: t, q: quotes[t] }))
    .sort((a, b) => Math.abs(b.q.changePct || 0) - Math.abs(a.q.changePct || 0));

  if (Object.keys(quotes).length === 0) return null;

  const Pill = ({ label, q, emphasize }) => {
    if (!q) return null;
    const chg = q.changePct;
    const color = chg == null ? 'var(--text-dim)' : chg >= 0 ? '#5fb37c' : '#d96a6a';
    const bg = emphasize && chg != null && Math.abs(chg) > 1.5
      ? (chg >= 0 ? 'rgba(95,179,124,0.12)' : 'rgba(217,106,106,0.12)')
      : 'transparent';
    return (
      <span style={{
        display: 'inline-flex', alignItems: 'center', gap: 6,
        padding: '4px 10px', fontSize: 11, fontFamily: "'JetBrains Mono', monospace",
        borderRadius: 3, background: bg,
        border: `1px solid ${chg != null && Math.abs(chg) > 1.5 ? color : 'var(--line)'}40`,
      }}>
        <span style={{ fontWeight: 700, color: 'var(--text)' }}>{label}</span>
        {q.price != null && <span style={{ color: 'var(--text-2)' }}>${Number(q.price).toFixed(2)}</span>}
        {chg != null && (
          <span style={{ color, fontWeight: 600 }}>
            {chg >= 0 ? '+' : ''}{chg.toFixed(2)}%
          </span>
        )}
      </span>
    );
  };

  return (
    <div style={{
      padding: '10px 14px', marginBottom: 14,
      background: 'var(--bg-2)', border: '1px solid var(--border-soft)',
      borderRadius: 4,
    }}>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center' }}>
        <span style={{
          fontSize: 10, letterSpacing: '0.18em', textTransform: 'uppercase',
          color: 'var(--accent)', fontWeight: 700, marginRight: 6,
        }}>
          Macro
        </span>
        {macroQuotes.map(({ symbol, q }) => <Pill key={symbol} label={symbol} q={q} />)}
      </div>
      {gapMovers.length > 0 && (
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center', marginTop: 8 }}>
          <span style={{
            fontSize: 10, letterSpacing: '0.18em', textTransform: 'uppercase',
            color: '#d9a85f', fontWeight: 700, marginRight: 6,
          }}>
            Gap movers · book
          </span>
          {gapMovers.map(({ symbol, q }) => <Pill key={symbol} label={`$${symbol}`} q={q} emphasize />)}
        </div>
      )}
    </div>
  );
}


// Phase 6 R · #1 — Catalyst calendar strip. Pre-trade context: what
// macro events are coming up in the next 30 days (FOMC, CPI, NFP,
// OPEX). Renders as a horizontal scroll of pills color-coded by impact.
// Data from /api/catalysts/upcoming. Per-ticker earnings coverage is
// future work (needs an external earnings API integration).
function CatalystStrip() {
  const [items, setItems] = tUseState([]);
  const [loaded, setLoaded] = tUseState(false);
  tUseEffect(() => {
    let cancel = false;
    fetch('/api/catalysts/upcoming?days=30')
      .then(r => r.ok ? r.json() : null)
      .then(j => { if (!cancel && j) { setItems(j.items || []); setLoaded(true); } })
      .catch(() => {});
    return () => { cancel = true; };
  }, []);
  if (!loaded || items.length === 0) return null;
  return (
    <div style={{
      display: 'flex', gap: 8, alignItems: 'center', padding: '10px 14px',
      marginBottom: 14, background: 'var(--bg-2)',
      border: '1px solid var(--border-soft)', borderRadius: 4,
      overflowX: 'auto', whiteSpace: 'nowrap',
    }}>
      <span style={{
        fontSize: 10, letterSpacing: '0.18em', textTransform: 'uppercase',
        color: 'var(--accent)', fontWeight: 700, marginRight: 6, flexShrink: 0,
      }}>
        Catalysts · 30d
      </span>
      {items.map((it, i) => {
        const high = it.impact_high;
        const days = it.days_out;
        const urgency = days <= 3 ? 'high' : days <= 7 ? 'medium' : 'low';
        const colors = {
          high:   high ? { bg: 'rgba(217,106,106,0.15)', fg: '#d96a6a', bord: 'rgba(217,106,106,0.4)' }
                       : { bg: 'rgba(217,168,95,0.12)', fg: '#d9a85f', bord: 'rgba(217,168,95,0.35)' },
          medium: high ? { bg: 'rgba(217,168,95,0.15)', fg: '#d9a85f', bord: 'rgba(217,168,95,0.4)' }
                       : { bg: 'rgba(120,140,180,0.12)', fg: 'var(--text-2)', bord: 'var(--line)' },
          low:    { bg: 'transparent', fg: 'var(--text-dim)', bord: 'var(--line)' },
        };
        const c = colors[urgency] || colors.low;
        return (
          <span key={i} title={`${it.label} · ${it.date} · ${high ? 'high impact' : 'normal impact'}`}
                style={{
                  display: 'inline-flex', alignItems: 'center', gap: 6,
                  padding: '4px 10px', fontSize: 11,
                  fontFamily: "'JetBrains Mono', monospace",
                  borderRadius: 3, flexShrink: 0,
                  background: c.bg, color: c.fg, border: `1px solid ${c.bord}`,
                }}>
            <span style={{ fontWeight: 700 }}>{days === 0 ? 'TODAY' : `${days}d`}</span>
            <span>{it.label}</span>
            {high && <span style={{ fontSize: 9 }}>★</span>}
          </span>
        );
      })}
    </div>
  );
}

function CascadeStrip({ data }) {
  // Render the cascade as a horizontal timeline. PhD → brief → Hedging,
  // each with a status dot. Lets you eyeball "did everything fire today"
  // without reading the per-section status logs.
  const items = [];
  // PhD (or any pre-brief persona via cron sorted earliest)
  for (const role of (data.cascade_order || [])) {
    const p = data.personas[role];
    if (!p) continue;
    items.push({
      kind: 'persona',
      role,
      title: p.title,
      cron: p.cron,
      outcome: p.last_run_outcome,
      memo_count: p.memo_count,
    });
  }
  // Brief slot — synthetic, no cron, but key milestone in the day
  const briefStatus = data.brief?.status || {};
  const briefOutcome = data.brief?.exists ? (data.brief.emailed ? 'ok' : (briefStatus.alerted_failure ? 'fail' : 'partial')) : null;
  // Insert brief between pre-brief and post-brief personas (sort by cron time)
  const briefIdx = items.findIndex(i => {
    if (!i.cron) return false;
    const [m, h] = i.cron.split(' ');
    const min = parseInt(h, 10) * 60 + parseInt(m, 10);
    return min > 6 * 60;  // brief fires at 06:00 ET-ish
  });
  const briefItem = {
    kind: 'brief',
    role: 'brief',
    title: 'Morning Brief',
    cron: '06:00',
    outcome: briefOutcome,
    memo_count: data.brief?.exists ? 1 : 0,
  };
  if (briefIdx === -1) items.push(briefItem);
  else items.splice(briefIdx, 0, briefItem);

  return (
    <div className="cascade-strip">
      {items.map((it, i) => (
        <React.Fragment key={i}>
          <div className="cascade-node" title={`${it.title}\nCron: ${it.cron || '—'}\nLast: ${it.outcome || 'not run'}`}>
            <div className="cascade-dot" style={{ background: outcomeColor(it.outcome) }} />
            <div className="cascade-label">
              <div style={{ fontWeight: 600 }}>{it.title.split(' (')[0]}</div>
              <div style={{ fontSize: 10, color: 'var(--text-dim)' }}>{it.cron || '—'}</div>
            </div>
          </div>
          {i < items.length - 1 && <div className="cascade-arrow">→</div>}
        </React.Fragment>
      ))}
      {items.length === 0 && (
        <span style={{ color: 'var(--text-dim)', fontSize: 12 }}>
          No cron-scheduled personas today.
        </span>
      )}
    </div>
  );
}

// Phase 6 R · #2 — Action row beneath each memo. Converts the Today
// tab from a passive cascade viewer into an actionable launch pad.
//
// Every memo gets:
//   - Open in Brain   (jumps to brain tab with the memo file open)
//   - Mark accepted   (👍, persists in localStorage so the chip can
//                       fade after acknowledgement)
//   - Mark rejected   (👎, same)
//
// Hedging memos additionally get:
//   - Stage hedge in Trades (jumps to portfolio tab — future: parses
//                           the memo for a hedge structure suggestion)
//
// PhD memos additionally get:
//   - Open in Research (creates a new web seeded for the memo's topic)
function MemoActions({ role, memo }) {
  const memoKey = `tibeb.memoFeedback.${memo.filename}`;
  const [feedback, setFeedback] = tUseState(() => {
    try { return localStorage.getItem(memoKey) || ''; } catch { return ''; }
  });
  const setFb = (v) => {
    setFeedback(v);
    try { localStorage.setItem(memoKey, v); } catch {}
    // Best-effort backend hint — gives the agent system a signal but
    // we don't block on it (endpoint may not exist; that's fine).
    fetch(`/api/personas/${encodeURIComponent(role)}/feedback`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ memo_filename: memo.filename, verdict: v, ts: new Date().toISOString() }),
    }).catch(() => {});
  };
  const openInBrain = () => {
    try {
      // Memos live at brain/0-Agents/Memory/{role}/{filename}
      const path = `0-Agents/Memory/${role}/${memo.filename}`;
      localStorage.setItem('jarvis.brain.openPath', path);
      localStorage.setItem('jarvis.activeTab', 'brain');
      location.reload();
    } catch {}
  };
  const stageHedge = () => {
    try {
      // Future: parse memo for a hedge structure suggestion. For now,
      // just jump to Trades tab so the user can act on what they read.
      localStorage.setItem('jarvis.activeTab', 'portfolio');
      location.reload();
    } catch {}
  };
  const openInResearch = () => {
    try {
      localStorage.setItem('jarvis.activeTab', 'research');
      location.reload();
    } catch {}
  };
  const btnStyle = {
    padding: '4px 10px', fontSize: 10,
    fontFamily: "'JetBrains Mono', monospace",
    background: 'var(--bg-1, #1c1a17)', color: 'var(--text-2)',
    border: '1px solid var(--border-soft)', borderRadius: 3,
    cursor: 'pointer',
  };
  return (
    <div style={{
      display: 'flex', alignItems: 'center', gap: 8,
      paddingTop: 10, marginTop: 10,
      borderTop: '1px solid var(--border-soft)',
    }}>
      <button style={btnStyle} onClick={openInBrain}>Open in Brain</button>
      {role === 'hedging-specialist' && (
        <button style={{ ...btnStyle, color: '#d9a85f', borderColor: 'rgba(217,168,95,0.4)' }}
                onClick={stageHedge}>
          Stage hedge in Trades →
        </button>
      )}
      {role === 'phd-researcher' && (
        <button style={{ ...btnStyle, color: '#88ddaa', borderColor: 'rgba(136,221,170,0.4)' }}
                onClick={openInResearch}>
          Open in Research →
        </button>
      )}
      <span style={{ marginLeft: 'auto', display: 'flex', gap: 6, alignItems: 'center' }}>
        {feedback && <span style={{ fontSize: 10, color: 'var(--text-dim)' }}>marked {feedback}</span>}
        <button style={{
          ...btnStyle,
          background: feedback === 'accepted' ? 'rgba(95,179,124,0.18)' : btnStyle.background,
          color: feedback === 'accepted' ? '#5fb37c' : btnStyle.color,
        }} onClick={() => setFb(feedback === 'accepted' ? '' : 'accepted')}
        title="Mark this memo as accepted (verdict feels right)">
          👍
        </button>
        <button style={{
          ...btnStyle,
          background: feedback === 'rejected' ? 'rgba(217,106,106,0.18)' : btnStyle.background,
          color: feedback === 'rejected' ? '#d96a6a' : btnStyle.color,
        }} onClick={() => setFb(feedback === 'rejected' ? '' : 'rejected')}
        title="Mark this memo as rejected (you disagree with the verdict)">
          👎
        </button>
      </span>
    </div>
  );
}

function PersonaCard({ role, persona, expanded, onToggle }) {
  const memos = persona.memos || [];
  return (
    <div className="today-card">
      <div className="today-card-head" onClick={onToggle} style={{ cursor: 'pointer' }}>
        <span className="today-card-dot" style={{ background: outcomeColor(persona.last_run_outcome) }} />
        <span style={{ fontWeight: 600 }}>{persona.title}</span>
        <span style={{ marginLeft: 12, fontSize: 11, color: 'var(--text-dim)' }}>
          {persona.cron ? `cron ${persona.cron}` : 'on-demand'}
          {' · '}
          {memos.length} memo{memos.length === 1 ? '' : 's'} today
          {persona.last_run_outcome && ` · last ${persona.last_run_outcome}`}
        </span>
        <span style={{ marginLeft: 'auto', color: 'var(--text-dim)' }}>{expanded ? '▾' : '▸'}</span>
      </div>
      {expanded && (
        <div className="today-card-body">
          {memos.length === 0 && (
            <div style={{ color: 'var(--text-dim)', fontSize: 12, padding: '8px 0' }}>
              No memos filed today.
            </div>
          )}
          {memos.map((m, i) => (
            <div key={i} className="memo-block">
              <div className="memo-head">
                <span style={{ fontWeight: 600 }}>{m.title}</span>
                <span style={{ marginLeft: 'auto', fontSize: 10, color: 'var(--text-dim)' }}>
                  {fmtRelTime(m.ts)} · {m.filename}
                </span>
              </div>
              <MarkdownBlock source={m.body} />
              {/* Phase 6 R · #2 — Memo action row. Turn the Today tab
                  from a passive viewer into a launch pad. Hedging
                  memos get "Stage hedge" (jumps to Trades), all memos
                  get accept/reject feedback that posts back to the
                  cron memory for future reference. */}
              <MemoActions role={role} memo={m} />
            </div>
          ))}
          {persona.status && persona.status.attempts && (
            <details className="status-attempts">
              <summary style={{ fontSize: 11, color: 'var(--text-dim)', cursor: 'pointer' }}>
                {persona.status.attempts.length} attempt{persona.status.attempts.length === 1 ? '' : 's'} today (raw log)
              </summary>
              <div style={{ marginTop: 6, fontSize: 11, fontFamily: "'JetBrains Mono', monospace" }}>
                {persona.status.attempts.slice(-12).map((a, i) => (
                  <div key={i} style={{ padding: '2px 0' }}>
                    <span style={{ color: outcomeColor(a.outcome), display: 'inline-block', width: 60 }}>
                      [{a.outcome}]
                    </span>
                    {' '}<span style={{ color: 'var(--text-dim)' }}>{(a.ts || '').slice(11, 19)}Z</span>
                    {' '}{a.trigger || ''}
                    {a.duration_s != null && <span style={{ color: 'var(--text-dim)' }}> · {a.duration_s}s</span>}
                    {a.error && <div style={{ color: '#d96a6a', paddingLeft: 70 }}>{a.error}</div>}
                  </div>
                ))}
              </div>
            </details>
          )}
        </div>
      )}
    </div>
  );
}

// AutonomyStrip — top-of-Today panel that surfaces the event-driven
// autonomy state: latest market snapshot (VIX, drawdown, BWD), every
// persona with a `trigger:` and its armed/latched state, and the count
// of pending trade-recs queued via propose_trade.
//
// Reads:
//   GET /api/events/snapshot?refresh=true
//   GET /api/events/personas
//   GET /api/trade-recs?status=pending&limit=50
//
// Polls every 60s while visible. Mirrors OvernightStrip's pill style.
function AutonomyStrip() {
  const [snapshot, setSnapshot] = tUseState(null);
  const [personas, setPersonas] = tUseState([]);
  const [pendingRecs, setPendingRecs] = tUseState([]);
  const [twitterSignals, setTwitterSignals] = tUseState([]);
  const [twitterPollerLive, setTwitterPollerLive] = tUseState(false);
  const [error, setError] = tUseState(null);

  const load = tUseCallback(async () => {
    try {
      const [snap, perso, recs, sigs, watch] = await Promise.all([
        fetch('/api/events/snapshot?refresh=true').then(r => r.ok ? r.json() : null).catch(() => null),
        fetch('/api/events/personas').then(r => r.ok ? r.json() : null).catch(() => null),
        fetch('/api/trade-recs?status=pending&limit=50').then(r => r.ok ? r.json() : null).catch(() => null),
        fetch('/api/twitter/signals?limit=10&routed_only=true').then(r => r.ok ? r.json() : null).catch(() => null),
        fetch('/api/twitter/watch').then(r => r.ok ? r.json() : null).catch(() => null),
      ]);
      setSnapshot(snap?.snapshot || null);
      setPersonas(perso?.personas || []);
      setPendingRecs(recs?.recs || []);
      setTwitterSignals(sigs?.signals || []);
      setTwitterPollerLive(Boolean(watch?.poller_live));
      setError(null);
    } catch (e) {
      setError(String(e.message || e));
    }
  }, []);

  tUseEffect(() => { load(); }, [load]);

  tUseEffect(() => {
    let t;
    const onVis = () => {
      if (document.visibilityState === 'visible') {
        t = setInterval(load, 60_000);
      } else if (t) {
        clearInterval(t); t = null;
      }
    };
    onVis();
    document.addEventListener('visibilitychange', onVis);
    return () => {
      if (t) clearInterval(t);
      document.removeEventListener('visibilitychange', onVis);
    };
  }, [load]);

  // No data yet → render nothing (matches OvernightStrip's pattern of
  // staying invisible until it has something to show).
  if (!snapshot && personas.length === 0 && pendingRecs.length === 0 && !error) return null;

  const fmtPct = (v) => v == null ? '—' : `${(v * 100).toFixed(1)}%`;
  const fmtNum = (v, digits = 0) => v == null ? '—' : Number(v).toLocaleString('en-US', { maximumFractionDigits: digits });

  // Snapshot pills — one per metric. Color the value based on whether
  // any wired trigger uses it as LHS and is currently true (rough
  // heuristic: "this metric matters and the threshold is hit").
  const snapColor = (field, value) => {
    if (value == null) return 'var(--text-dim)';
    const matchingTrigger = personas.find(p =>
      p.trigger && p.trigger.toLowerCase().startsWith(field) && p.evaluates_now === true);
    if (matchingTrigger) return '#d96a6a';
    return 'var(--text-2)';
  };

  return (
    <div style={{
      padding: '12px 14px', marginBottom: 14,
      background: 'var(--bg-2)', border: '1px solid var(--border-soft)',
      borderRadius: 4,
    }}>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 10, alignItems: 'center' }}>
        <span style={{
          fontSize: 10, letterSpacing: '0.18em', textTransform: 'uppercase',
          color: 'var(--accent)', fontWeight: 700, marginRight: 6,
        }}>
          Autonomy
        </span>

        {/* Snapshot metric pills */}
        {snapshot && (
          <>
            <Metric label="VIX"        value={fmtNum(snapshot.vix, 2)}        color={snapColor('vix', snapshot.vix)} />
            <Metric label="DD"         value={fmtPct(snapshot.drawdown_pct)}  color={snapColor('drawdown_pct', snapshot.drawdown_pct)} />
            <Metric label="BWD"        value={`$${fmtNum(snapshot.beta_weighted_delta, 0)}`} color={snapColor('beta_weighted_delta', snapshot.beta_weighted_delta)} />
            <Metric label="Concen"     value={fmtPct(snapshot.largest_concentration_pct)} color={snapColor('largest_concentration_pct', snapshot.largest_concentration_pct)} />
            <Metric label="MV"         value={`$${fmtNum(snapshot.portfolio_value, 0)}`} color="var(--text-2)" />
          </>
        )}

        {/* Pending recs counter — clickable would be great but the
            Trades tab is a separate workspace; for now just show count. */}
        {pendingRecs.length > 0 && (
          <span style={{
            display: 'inline-flex', alignItems: 'center', gap: 6,
            padding: '4px 10px', fontSize: 11, fontFamily: "'JetBrains Mono', monospace",
            borderRadius: 3, background: 'rgba(217,168,95,0.10)',
            border: '1px solid rgba(217,168,95,0.4)', marginLeft: 'auto',
          }}>
            <span style={{ fontWeight: 700, color: '#d9a85f' }}>{pendingRecs.length} queued</span>
            <span style={{ color: 'var(--text-dim)' }}>· review in Trades</span>
          </span>
        )}
      </div>

      {/* Trigger row — one pill per persona */}
      {personas.length > 0 && (
        <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, alignItems: 'center', marginTop: 10 }}>
          <span style={{
            fontSize: 10, letterSpacing: '0.18em', textTransform: 'uppercase',
            color: 'var(--text-dim)', fontWeight: 700, marginRight: 4,
          }}>
            Triggers
          </span>
          {personas.map(p => <TriggerPill key={p.role} p={p} />)}
        </div>
      )}

      {/* Twitter signals row — recent matched tweets that fired a persona */}
      {twitterSignals.length > 0 && (
        <div style={{ marginTop: 10 }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
            <span style={{
              fontSize: 10, letterSpacing: '0.18em', textTransform: 'uppercase',
              color: 'var(--text-dim)', fontWeight: 700,
            }}>
              Twitter
            </span>
            <span style={{
              fontSize: 9, color: twitterPollerLive ? '#5fb37c' : 'var(--text-dim)',
              fontFamily: "'JetBrains Mono', monospace",
            }}>
              {twitterPollerLive ? '● live' : '○ manual-only'}
            </span>
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
            {twitterSignals.slice(0, 6).map(s => <TwitterSignalRow key={s.tweet_id} s={s} />)}
          </div>
        </div>
      )}

      {error && (
        <div style={{ marginTop: 8, color: '#d96a6a', fontSize: 11, fontFamily: "'JetBrains Mono', monospace" }}>
          {error}
        </div>
      )}
    </div>
  );
}

function Metric({ label, value, color }) {
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center', gap: 6,
      padding: '4px 10px', fontSize: 11, fontFamily: "'JetBrains Mono', monospace",
      borderRadius: 3, border: '1px solid var(--line)40',
    }}>
      <span style={{ fontWeight: 700, color: 'var(--text)' }}>{label}</span>
      <span style={{ color, fontWeight: 600 }}>{value}</span>
    </span>
  );
}

function TwitterSignalRow({ s }) {
  // Per-sector dot colors so a glance at the row tells you what the
  // signal is about without reading the text. Falls through to a neutral
  // dot for uncategorized handles (e.g., macro accounts).
  const SECTOR_COLOR = {
    'semiconductors': '#9b7acc',
    'ai-ml':          '#5fb37c',
    'cloud-saas':     '#7ab3d9',
    'energy':         '#d9a85f',
    'macro':          '#d96a6a',
    'earnings':       '#cc7ab3',
    'cybersecurity':  '#7ad9b3',
    'batteries':      '#b3d97a',
    'space':          '#7a8bd9',
  };
  const dotColor = SECTOR_COLOR[s.sector] || 'var(--text-dim)';
  return (
    <a href={`https://twitter.com/${s.handle}/status/${s.tweet_id}`}
       target="_blank" rel="noopener noreferrer"
       style={{
         display: 'flex', alignItems: 'baseline', gap: 8,
         padding: '4px 10px', fontSize: 11,
         fontFamily: "'JetBrains Mono', monospace",
         borderRadius: 3, background: 'var(--bg-3, #1f1c19)',
         border: '1px solid var(--line)20',
         color: 'inherit', textDecoration: 'none',
       }}
       title={s.text + (s.sector ? `\nsector: ${s.sector}` : '')}>
      <span style={{
        display: 'inline-block', width: 6, height: 6, borderRadius: 3,
        background: dotColor, flexShrink: 0,
      }} />
      <span style={{ color: '#5fb37c', fontWeight: 700, minWidth: 110 }}>@{s.handle}</span>
      {s.sector && (
        <span style={{
          color: dotColor, fontSize: 9, fontWeight: 700,
          letterSpacing: '0.08em', textTransform: 'uppercase',
          minWidth: 80,
        }}>
          {s.sector}
        </span>
      )}
      <span style={{ color: 'var(--text-dim)', minWidth: 90 }}>→ {s.routed_to || '(unrouted)'}</span>
      <span style={{ flex: 1, color: 'var(--text-2)', overflow: 'hidden',
                     textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
        {s.text}
      </span>
      <span style={{ color: 'var(--text-dim)', fontSize: 10 }}>
        {fmtRelTime(s.created)}
      </span>
    </a>
  );
}

function TriggerPill({ p }) {
  // Color logic:
  //   evaluates_now=true + armed=true    → red (will fire on next tick)
  //   evaluates_now=true + armed=false   → amber (latched, in cooldown)
  //   evaluates_now=false + armed=true   → dim (waiting)
  //   evaluates_now=null                 → gray (data unavailable)
  let color = 'var(--text-dim)';
  let bg = 'transparent';
  if (p.evaluates_now === true && p.armed) {
    color = '#d96a6a'; bg = 'rgba(217,106,106,0.10)';
  } else if (p.evaluates_now === true && !p.armed) {
    color = '#d9a85f'; bg = 'rgba(217,168,95,0.10)';
  } else if (p.evaluates_now === false && p.armed) {
    color = '#5fb37c';
  }
  const lastFired = p.last_fired_ts ? fmtRelTime(p.last_fired_ts) : null;
  return (
    <span
      title={`${p.title}\ntrigger: ${p.trigger}\ncooldown: ${p.cooldown_min}min${lastFired ? `\nlast fired ${lastFired}` : ''}`}
      style={{
        display: 'inline-flex', alignItems: 'center', gap: 6,
        padding: '3px 8px', fontSize: 10, fontFamily: "'JetBrains Mono', monospace",
        borderRadius: 3, background: bg,
        border: `1px solid ${color}50`,
      }}
    >
      <span style={{ fontWeight: 700, color }}>{p.role}</span>
      <span style={{ color: 'var(--text-2)' }}>{p.trigger}</span>
      {!p.armed && <span style={{ color: '#d9a85f', fontWeight: 700 }}>·latched</span>}
      {p.evaluates_now === true && p.armed && <span style={{ color: '#d96a6a', fontWeight: 700 }}>·hot</span>}
    </span>
  );
}


function TodayWorkspace() {
  const [data, setData] = tUseState(null);
  const [loading, setLoading] = tUseState(true);
  const [error, setError] = tUseState(null);
  const [date, setDate] = tUseState(todayDateISO);
  const [expanded, setExpanded] = tUseState({});  // {role: bool}
  const [briefOpen, setBriefOpen] = tUseState(true);
  const [lastFetch, setLastFetch] = tUseState(null);

  const load = tUseCallback(async () => {
    setLoading(true);
    try {
      const r = await fetch(`/api/today?date=${encodeURIComponent(date)}`);
      if (!r.ok) throw new Error(`HTTP ${r.status}`);
      const d = await r.json();
      setData(d);
      setError(null);
      setLastFetch(new Date().toISOString());
    } catch (e) {
      setError(String(e.message || e));
    } finally {
      setLoading(false);
    }
  }, [date]);

  tUseEffect(() => { load(); }, [load]);

  // Auto-poll every 60s while the tab is visible
  tUseEffect(() => {
    let t;
    const onVis = () => {
      if (document.visibilityState === 'visible') {
        t = setInterval(load, TODAY_POLL_MS);
      } else if (t) {
        clearInterval(t);
        t = null;
      }
    };
    onVis();
    document.addEventListener('visibilitychange', onVis);
    return () => {
      if (t) clearInterval(t);
      document.removeEventListener('visibilitychange', onVis);
    };
  }, [load]);

  const personasByCascade = tUseMemo(() => {
    if (!data) return [];
    const order = data.cascade_order || [];
    const seen = new Set(order);
    const rest = Object.keys(data.personas || {}).filter(r => !seen.has(r));
    return [...order, ...rest].map(r => [r, data.personas[r]]).filter(([, p]) => p);
  }, [data]);

  const triggerRegen = tUseCallback(async () => {
    if (!confirm('Force a full brief regen? Returns 202 immediately, runs ~2 min in background.')) return;
    try {
      const r = await fetch('/api/morning-brief/generate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ force: true, regen: true }),
      });
      const d = await r.json();
      alert(d.message || JSON.stringify(d));
      // Poll twice over the next ~3 min so the new brief shows up
      setTimeout(load, 90_000);
      setTimeout(load, 180_000);
    } catch (e) {
      alert(`Regen request failed: ${e}`);
    }
  }, [load]);

  return (
    <div className="today-workspace">
      <style>{`
        .today-workspace {
          /* Phase 6 R · UI fix — span the full grid + handle scrolling
             ourselves. Was rendering inside a single 240px grid column
             with no overflow, which broke scrolling and squished the
             content. Mirrors .portfolio-workspace + .brain-workspace
             pattern. */
          grid-column: 1 / -1;
          height: calc(100vh - 56px - 36px);
          overflow-y: auto;
          padding: 24px 32px 80px;
          color: var(--text);
          font-family: var(--font-body);
        }
        .today-workspace > * { max-width: 1100px; margin-left: auto; margin-right: auto; }
        .today-header {
          display: flex; align-items: baseline; gap: 14px;
          padding-bottom: 14px; margin-bottom: 18px;
          border-bottom: 1px solid var(--border-soft);
        }
        .today-header h1 {
          font-family: var(--font-serif, Georgia, serif);
          font-style: italic; font-size: 26px; margin: 0;
        }
        .today-header .meta {
          color: var(--text-dim); font-size: 12px;
          font-family: 'JetBrains Mono', monospace;
        }
        .today-header input[type=date] {
          background: var(--bg-2); color: var(--text);
          border: 1px solid var(--border-soft);
          padding: 4px 8px; font-family: 'JetBrains Mono', monospace;
          font-size: 12px;
        }
        .today-header button {
          background: var(--bg-2); color: var(--text);
          border: 1px solid var(--border-soft);
          padding: 4px 10px; cursor: pointer;
          font-family: 'JetBrains Mono', monospace; font-size: 11px;
        }
        .today-header button:hover { background: var(--bg-3, #2a2622); }
        .cascade-strip {
          display: flex; align-items: center; gap: 8px;
          padding: 14px 16px; background: var(--bg-2);
          border: 1px solid var(--border-soft); border-radius: 4px;
          margin-bottom: 18px; flex-wrap: wrap;
        }
        .cascade-node { display: flex; align-items: center; gap: 8px; }
        .cascade-dot { width: 10px; height: 10px; border-radius: 50%; }
        .cascade-label { font-size: 11px; line-height: 1.3; }
        .cascade-arrow { color: var(--text-dim); font-size: 14px; }
        .today-card {
          background: var(--bg-2); border: 1px solid var(--border-soft);
          border-radius: 4px; margin-bottom: 12px; overflow: hidden;
        }
        .today-card-head {
          display: flex; align-items: center; gap: 10px;
          padding: 12px 16px; user-select: none;
        }
        .today-card-head:hover { background: rgba(255,255,255,0.02); }
        .today-card-dot { width: 8px; height: 8px; border-radius: 50%; }
        .today-card-body { padding: 0 16px 16px 16px; border-top: 1px solid var(--border-soft); padding-top: 12px; }
        .memo-block { margin: 10px 0; padding: 10px 14px; background: var(--bg-1, #1c1a17); border-radius: 3px; }
        .memo-head { display: flex; align-items: center; padding-bottom: 6px; border-bottom: 1px solid var(--border-soft); margin-bottom: 8px; font-size: 12px; }
        .today-md { font-size: 13px; line-height: 1.55; }
        .today-md h1, .today-md h2, .today-md h3 { font-family: var(--font-serif, Georgia, serif); margin-top: 14px; margin-bottom: 6px; }
        .today-md h1 { font-size: 18px; }
        .today-md h2 { font-size: 15px; }
        .today-md h3 { font-size: 13px; font-style: italic; }
        .today-md table { border-collapse: collapse; margin: 8px 0; font-size: 12px; }
        .today-md th, .today-md td { border: 1px solid var(--border-soft); padding: 4px 8px; }
        .today-md code { background: var(--bg-1, #1c1a17); padding: 1px 4px; font-size: 11px; }
        .today-md pre { background: var(--bg-1, #1c1a17); padding: 10px; overflow-x: auto; font-size: 11px; }
        .today-md blockquote { border-left: 3px solid var(--accent, #5fb37c); padding-left: 12px; color: var(--text-dim); margin: 8px 0; }
        .empty-state { color: var(--text-dim); font-style: italic; padding: 32px; text-align: center; }
      `}</style>

      <div className="today-header">
        <h1>Today</h1>
        <span className="meta">{date}</span>
        <input type="date" value={date} onChange={e => setDate(e.target.value)} />
        <button onClick={load} disabled={loading}>{loading ? '…' : 'refresh'}</button>
        <button onClick={triggerRegen} title="Force a fresh brief regen (async, ~2 min)">regen brief</button>
        {lastFetch && <span className="meta" style={{ marginLeft: 'auto' }}>updated {fmtRelTime(lastFetch)}</span>}
      </div>

      {error && (
        <div style={{ color: '#d96a6a', padding: 12, background: 'var(--bg-2)', border: '1px solid #d96a6a', marginBottom: 14 }}>
          Failed to load /api/today: {error}
        </div>
      )}

      {data && (
        <>
          <AutonomyStrip />
          <OvernightStrip />
          <CatalystStrip />
          <CascadeStrip data={data} />

          {/* Brief — main event */}
          <div className="today-card">
            <div className="today-card-head" onClick={() => setBriefOpen(!briefOpen)} style={{ cursor: 'pointer' }}>
              <span className="today-card-dot" style={{
                background: data.brief.exists
                  ? (data.brief.emailed ? '#5fb37c' : '#d9a85f')
                  : 'var(--text-dim)'
              }} />
              <span style={{ fontWeight: 600 }}>Morning Brief</span>
              <span style={{ marginLeft: 12, fontSize: 11, color: 'var(--text-dim)' }}>
                {data.brief.exists
                  ? `${data.brief.body_chars.toLocaleString()} chars · emailed: ${data.brief.emailed ? `yes (${data.brief.emailed_via})` : 'no'}`
                  : 'not generated yet'}
              </span>
              <span style={{ marginLeft: 'auto', color: 'var(--text-dim)' }}>{briefOpen ? '▾' : '▸'}</span>
            </div>
            {briefOpen && (
              <div className="today-card-body">
                {data.brief.body_md
                  ? <MarkdownBlock source={data.brief.body_md} />
                  : <div className="empty-state">No brief on disk for {date}. Hit “regen brief” above (returns immediately, runs in background).</div>}
              </div>
            )}
          </div>

          {/* Personas */}
          {personasByCascade.length === 0 && (
            <div className="empty-state">No persona memos for {date}.</div>
          )}
          {personasByCascade.map(([role, p]) => (
            <PersonaCard
              key={role}
              role={role}
              persona={p}
              expanded={!!expanded[role]}
              onToggle={() => setExpanded({ ...expanded, [role]: !expanded[role] })}
            />
          ))}
        </>
      )}

      {!data && !error && loading && (
        <div className="empty-state">Loading today’s cascade…</div>
      )}
    </div>
  );
}

window.TodayWorkspace = TodayWorkspace;
