/* =============================================================================
   Components — Options Scanner
   ===========================================================================*/

const { useState, useEffect, useMemo, useRef } = React;

/* ----- Icons --------------------------------------------------------------- */
const Icon = {
  Logo:    () => <svg viewBox="0 0 24 24" fill="none"><path d="M4 16 L9 9 L13 14 L20 6" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"/><circle cx="20" cy="6" r="1.6" fill="currentColor"/></svg>,
  Sun:     () => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M2 12h2M20 12h2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>,
  Moon:    () => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/></svg>,
  Search:  () => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3" strokeLinecap="round"/></svg>,
  Refresh: () => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"><path d="M21 12a9 9 0 1 1-3-6.7"/><path d="M21 4v5h-5"/></svg>,
  Close:   () => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M6 6l12 12M18 6L6 18"/></svg>,
  List:    () => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M4 6h16M4 12h16M4 18h16"/></svg>,
  Group:   () => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg>,
  Arrow:   () => <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12h14M13 6l6 6-6 6"/></svg>,
};

/* ----- Helpers ------------------------------------------------------------- */
const fmtMoney = (n) => {
  if (n === null || n === undefined || Number.isNaN(n)) return "—";
  const sign = n < 0 ? "-" : "";
  const abs = Math.abs(n);
  return `${sign}$${abs.toFixed(2)}`;
};
const fmtPct = (n) => (n === null || n === undefined ? "—" : `${(n * 100).toFixed(1)}%`);

// Null-safe formatters. The design's mock data always had a value at every
// field; our DuckDB rows can legitimately be null (Greeks before a snapshot
// is bound to the leg, sparklines before multi-day history accumulates,
// etc), so the drawer + grouped view need to tolerate them without crashing.
const fmtNum = (n, decimals = 2) => (n === null || n === undefined ? "—" : n.toFixed(decimals));
const fmtPrice = (n) => (n === null || n === undefined ? "—" : `$${n.toFixed(2)}`);

// Per-CONTRACT dollar formatter. The data layer stores per-share P/L
// (credit, max profit, max loss) because that's what the underlying
// options math computes. Brokerages quote per-contract (= per-share × 100),
// so display columns in the table and drawer use this formatter for
// dollar amounts that flow as cash. Strikes/breakeven/underlying stay
// per-share since those are stock prices, not P/L flows.
const fmtContractMoney = (n) => {
  if (n === null || n === undefined || Number.isNaN(n)) return "—";
  const sign = n < 0 ? "-" : "";
  const abs = Math.abs(n) * 100;
  return `${sign}$${abs.toFixed(0)}`;
};

/* ----- Sparkline ----------------------------------------------------------- */
function Sparkline({ data, width = 80, height = 28, stroke, fill }) {
  // Need at least 2 points; otherwise step = width / 0 = Infinity and the
  // SVG renders nothing visible. Common when scan history is just starting
  // to accumulate (one snapshot per day per ticker).
  if (!data || data.length < 2) return null;
  const min = Math.min(...data), max = Math.max(...data);
  const range = max - min || 1;
  const step = width / (data.length - 1);
  const points = data.map((v, i) => [i * step, height - ((v - min) / range) * (height - 4) - 2]);
  const pathD = points.map((p, i) => `${i === 0 ? "M" : "L"} ${p[0].toFixed(2)} ${p[1].toFixed(2)}`).join(" ");
  const areaD = `${pathD} L ${width} ${height} L 0 ${height} Z`;
  const isUp = data[data.length - 1] >= data[0];
  const c = stroke || (isUp ? "var(--bull)" : "var(--bear)");
  const f = fill || (isUp ? "color-mix(in oklab, var(--bull) 14%, transparent)" : "color-mix(in oklab, var(--bear) 14%, transparent)");
  return (
    <svg className="spark-svg" viewBox={`0 0 ${width} ${height}`} preserveAspectRatio="none">
      <path d={areaD} fill={f} />
      <path d={pathD} fill="none" stroke={c} strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" />
    </svg>
  );
}

/* ----- Strategy pill ------------------------------------------------------- */
// The glossary popover that used to live here was retired in favor of the
// centralized ? help modal in the top bar. Keeping the styled pill but
// dropping the cursor:help + popover keeps the UI quiet without losing
// the strategy → color affordance.
function StratPill({ code }) {
  return (
    <span className={`strat-pill ${code}`} data-strategy={code}>
      {code}
    </span>
  );
}

/* ----- Confluence Spectrum ------------------------------------------------- */
function ConfluenceSpectrum({ ideas, onPick, hoveredId, setHoveredId }) {
  const min = 0.5, max = 0.9;
  const top = ideas.reduce((a, b) => (a.confluence > b.confluence ? a : b));
  const placed = useMemo(() => ideas.map(i => ({
    ...i,
    pct: Math.max(0, Math.min(1, (i.confluence - min) / (max - min))) * 100,
  })), [ideas]);

  return (
    <div className="card spectrum-card">
      <div className="spectrum-head">
        <div className="card-label">
          <span className="card-label-dot" />
          Confluence Spectrum
        </div>
        <div className="spectrum-meta">
          all <span className="mono tnum">{ideas.length}</span> ideas plotted by score
        </div>
      </div>
      <div className="spectrum-stage">
        {/* The "MP · 89.7%" floating callout was removed in Phase 4j —
            the pulsing is-top dot already differentiates the leader,
            and the same info shows on hover via the per-dot tooltip
            ("MP LC · 89.7%"). The callout was redundant. */}
        <div className="spectrum-axis">
          <div className="spectrum-tick" style={{ left: 0 }}>50%</div>
          <div className="spectrum-tick" style={{ left: "25%" }}>60%</div>
          <div className="spectrum-tick" style={{ left: "50%" }}>70%</div>
          <div className="spectrum-tick" style={{ left: "75%" }}>80%</div>
          <div className="spectrum-tick" style={{ left: "100%" }}>90%</div>
        </div>
        {placed.map(p => {
          const stratColor = `var(--${p.strategy.toLowerCase()})`;
          const isTop = p.id === top.id;
          const isHover = hoveredId === p.id;
          return (
            <div
              key={p.id}
              className={`spectrum-dot ${isTop ? "is-top" : ""}`}
              style={{
                left: `${p.pct}%`,
                background: stratColor,
                opacity: hoveredId && !isHover && !isTop ? 0.35 : 1,
              }}
              onClick={() => onPick(p.id)}
              onMouseEnter={() => setHoveredId(p.id)}
              onMouseLeave={() => setHoveredId(null)}
              title={`${p.ticker} ${p.strategy} · ${(p.confluence * 100).toFixed(1)}%`}
            />
          );
        })}
      </div>
    </div>
  );
}

/* ----- Hero ---------------------------------------------------------------- */

// Format a UTC ISO timestamp as the viewer's local time. Locale-aware so
// the year/month/day order matches whatever the browser is set to. Falls
// back to the legacy server-rendered string if the ISO field is missing.
function formatLocalScanTime(iso, fallback) {
  if (!iso) return fallback || "";
  const d = new Date(iso);
  if (isNaN(d.getTime())) return fallback || "";
  const date = d.toLocaleDateString(undefined, { year: "numeric", month: "short", day: "numeric" });
  const time = d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
  return `${date} · ${time}`;
}

function Hero({ ideas, onPick }) {
  const [hoveredId, setHoveredId] = useState(null);
  const counts = window.SCAN.strategyCounts;
  const diff = window.SCAN.diff;
  // Derive a single representative DTE/expiration to show in the subtitle.
  // Today every scan emits one expiration but that won't always be true,
  // so pick the most-frequent one rather than hardcoding "41 DTE / jun-18".
  const expCounts = ideas.reduce((m, i) => { if (i.exp) m.set(i.exp, (m.get(i.exp) || 0) + 1); return m; }, new Map());
  const topExp = expCounts.size ? [...expCounts.entries()].sort((a, b) => b[1] - a[1])[0] : null;
  const topIdea = topExp ? ideas.find(i => i.exp === topExp[0]) : null;
  const expLabel = topIdea
    ? `${topIdea.dte} DTE · ${new Date(topIdea.exp).toLocaleDateString(undefined, { month: "short", day: "numeric" }).toLowerCase()} chain`
    : "";
  return (
    <div className="hero">
      <div className="card">
        <div className="card-label"><span className="card-label-dot" /> Latest scan</div>
        <div className="summary-stat">
          <div className="summary-num tnum">{window.SCAN.totalIdeas}</div>
          <div className="summary-unit">ideas across {window.SCAN.totalTickers} tickers</div>
        </div>
        <div className="summary-sub mono">
          {formatLocalScanTime(window.SCAN.scannedAtIso, window.SCAN.scannedAt)}
          {expLabel && <> &nbsp;·&nbsp; {expLabel}</>}
        </div>
        <div className="strat-mix">
          {Object.entries(counts).map(([code, n]) => (
            <span key={code} className="strat-mix-item">
              <span className="strat-mix-dot" style={{ background: `var(--${code.toLowerCase()})` }} />
              <strong className="strat-mix-code">{code}</strong>
              <span className="strat-mix-count">{n}</span>
            </span>
          ))}
        </div>
      </div>

      <ConfluenceSpectrum
        ideas={ideas}
        onPick={onPick}
        hoveredId={hoveredId}
        setHoveredId={setHoveredId}
      />

      <div className="card">
        <div className="card-label"><span className="card-label-dot" /> Δ vs last scan</div>
        <DiffRow
          label="Newly added"
          count={diff.added}
          countClass="added"
          prefix="+"
          details={diff.addedDetails}
          emptyHint="No new ideas since last scan"
          accent="bull"
          onPick={onPick}
        />
        <DiffRow
          label="Dropped out"
          count={diff.removed}
          countClass="removed"
          prefix="−"
          details={diff.removedDetails}
          emptyHint="Nothing rotated out since last scan"
          accent="bear"
          /* dropped ideas no longer exist in the current scan, so
             onPick is intentionally omitted — clicking a dropped row
             can't open a drawer for an idea that isn't here. */
        />
        <div className="diff-row">
          <div>
            <div className="diff-label">Sustained</div>
            <div className="diff-num sustained tnum">{diff.sustained}</div>
          </div>
        </div>
      </div>
    </div>
  );
}

/* ----- Diff row with hover detail popover --------------------------------
 * The Δ-vs-last-scan card was previously a static count. Now each row
 * shows a hover popover listing the actual tickers + their confluence
 * scores so the operator can see WHO rotated in/out, not just HOW MANY.
 *
 * For "added": clicking a ticker in the popover scrolls to / opens that
 * idea's drawer (via the `onPick` callback wired to setSelectedId).
 * For "dropped": tickers are display-only — they're not in the current
 * scan, so we can't open their drawer.
 * --------------------------------------------------------------------------- */
function DiffRow({ label, count, countClass, prefix, details, emptyHint, accent, onPick }) {
  const items = details || [];
  const hasItems = items.length > 0;
  const findIdeaId = (d) => {
    // Match the synthesized id format from dashboard.py:_idea_id
    if (!window.IDEAS) return null;
    const match = window.IDEAS.find(i =>
      i.ticker === d.ticker && i.strategy === d.strategy && i.exp === d.exp
    );
    return match ? match.id : null;
  };
  return (
    <div className={`diff-row diff-row-popover ${hasItems ? "has-details" : ""}`}>
      <div className="diff-row-content">
        <div className="diff-label">{label}</div>
        <div className={`diff-num ${countClass} tnum`}>{prefix}{count}</div>
      </div>
      {hasItems && (
        <div className={`diff-popover diff-popover-${accent}`}>
          <div className="diff-popover-title">{label.toLowerCase()}</div>
          <ul className="diff-popover-list">
            {items.map((d, idx) => {
              const ideaId = onPick ? findIdeaId(d) : null;
              const interactive = !!ideaId;
              return (
                <li
                  key={`${d.ticker}-${d.strategy}-${idx}`}
                  className={interactive ? "is-clickable" : ""}
                  onClick={interactive ? () => onPick(ideaId) : undefined}
                >
                  <div className="diff-popover-line">
                    <span className="diff-popover-ticker mono">{d.ticker}</span>
                    <span className={`strat-pill ${d.strategy}`}>{d.strategy}</span>
                    <span className="diff-popover-conf mono tnum">
                      {d.confluence !== null && d.confluence !== undefined
                        ? `${(d.confluence * 100).toFixed(1)}%`
                        : "—"}
                    </span>
                  </div>
                  {/* Reason tag (dropped-only). For "added" details we
                      don't show a reason since "newly qualified" is the
                      implicit reason. */}
                  {d.reasonLabel && (
                    <div className={`diff-popover-reason reason-${d.reason}`}>
                      {d.reasonLabel}
                    </div>
                  )}
                </li>
              );
            })}
          </ul>
        </div>
      )}
      {!hasItems && (
        <div className="diff-popover">
          <div className="diff-popover-empty">{emptyHint}</div>
        </div>
      )}
    </div>
  );
}

/* ----- Filter Toolbar ------------------------------------------------------ */
function Toolbar({ activeStrat, setActiveStrat, view, setView, query, setQuery }) {
  const counts = window.SCAN.strategyCounts;
  const total = window.SCAN.totalIdeas;
  return (
    <div className="toolbar">
      <div className="chip-group">
        <button
          className={`chip ${activeStrat === "ALL" ? "active" : ""}`}
          onClick={() => setActiveStrat("ALL")}
        >
          All <span className="chip-count">{total}</span>
        </button>
        {Object.entries(counts).map(([code, n]) => (
          <button
            key={code}
            className={`chip ${activeStrat === code ? "active" : ""}`}
            onClick={() => setActiveStrat(code)}
          >
            <span className="chip-dot" style={{ background: `var(--${code.toLowerCase()})` }} />
            {code} <span className="chip-count">{n}</span>
          </button>
        ))}
      </div>

      <div className="view-toggle">
        <button className={view === "list" ? "active" : ""} onClick={() => setView("list")}>
          <Icon.List /> List
        </button>
        <button className={view === "group" ? "active" : ""} onClick={() => setView("group")}>
          <Icon.Group /> By ticker
        </button>
      </div>

      <div className="search">
        <Icon.Search />
        <input
          type="text" value={query}
          onChange={(e) => setQuery(e.target.value)}
          placeholder="Ticker..."
        />
      </div>
    </div>
  );
}

/* ----- Confluence trend (24h) ---------------------------------------------
 * `idea.confluenceHistory` is a list of {capturedAt, score} from prior
 * scans within 24h. We compare the oldest in-window score against the
 * current confluence to decide direction (↑ rising, ↓ falling, → flat),
 * and show a tiny arrow + delta in the table; the drawer renders the
 * full series as a sparkline.
 *
 * Hidden entirely when fewer than 2 prior datapoints exist (no real
 * trend) so the dashboard isn't cluttered with "—" placeholders early
 * in the day.
 * --------------------------------------------------------------------------- */
const TREND_FLAT_THRESHOLD = 0.005;  // ±0.5pp in confluence treated as flat

function _trendDirection(history, current) {
  if (!history || history.length < 1 || current === null || current === undefined) return null;
  const earliest = history[0].score;
  const delta = current - earliest;
  if (Math.abs(delta) < TREND_FLAT_THRESHOLD) return { dir: "flat", delta };
  return { dir: delta > 0 ? "up" : "down", delta };
}

function ConfluenceTrendArrow({ idea }) {
  const trend = _trendDirection(idea.confluenceHistory, idea.confluence);
  if (!trend) return null;
  const arrow = trend.dir === "up" ? "↑" : trend.dir === "down" ? "↓" : "→";
  const sign = trend.delta >= 0 ? "+" : "−";
  const pct = (Math.abs(trend.delta) * 100).toFixed(1);
  return (
    <span
      className={`conf-trend conf-trend-${trend.dir}`}
      title={`24h change: ${sign}${pct} pp (${idea.confluenceHistory.length} prior datapoint${idea.confluenceHistory.length === 1 ? "" : "s"})`}
    >
      {arrow}
    </span>
  );
}

function ConfluenceTrendSparkline({ idea }) {
  const history = idea.confluenceHistory || [];
  // Need at least 2 points to draw a meaningful trajectory; the
  // current confluence anchors the right edge of the line.
  if (history.length < 1 || idea.confluence === null) return null;
  const series = [...history.map(h => h.score), idea.confluence];
  if (series.length < 2) return null;
  const W = 200, H = 36, pad = 2;
  const min = Math.min(...series), max = Math.max(...series);
  const range = max - min || 0.001;
  const step = (W - 2 * pad) / (series.length - 1);
  const pathD = series.map((v, i) => {
    const x = pad + i * step;
    const y = pad + (1 - (v - min) / range) * (H - 2 * pad);
    return `${i === 0 ? "M" : "L"} ${x.toFixed(1)} ${y.toFixed(1)}`;
  }).join(" ");
  const lastY = pad + (1 - (idea.confluence - min) / range) * (H - 2 * pad);
  const lastX = pad + (series.length - 1) * step;
  const isUp = idea.confluence >= series[0];
  return (
    <div className="conf-trend-spark">
      <div className="conf-trend-spark-label">
        24h confluence trend
        <span className="conf-trend-spark-range mono tnum">
          {(min * 100).toFixed(1)}% – {(max * 100).toFixed(1)}%
        </span>
      </div>
      <svg viewBox={`0 0 ${W} ${H}`} className="conf-trend-spark-svg">
        <path d={pathD} fill="none" strokeWidth="1.5"
              stroke={isUp ? "var(--bull)" : "var(--bear)"} />
        <circle cx={lastX} cy={lastY} r="2.5"
                fill={isUp ? "var(--bull)" : "var(--bear)"} />
      </svg>
    </div>
  );
}

/* ----- Idea Row (in list view) -------------------------------------------- */
function IdeaRow({ idea, selected, onClick, onTrack }) {
  const isCredit = idea.credit > 0;
  const confPct = (idea.confluence * 100).toFixed(1);
  return (
    <tr
      className={`${selected ? "selected" : ""} ${idea.isNew ? "is-new" : ""}`}
      onClick={onClick}
    >
      <td>
        <div className="ticker-cell">
          {/* Stop propagation so clicking the ticker opens TradingView in
              a new tab without ALSO triggering the row's drawer onClick. */}
          <a
            className="ticker-symbol mono ticker-link"
            href={`https://www.tradingview.com/chart/?symbol=${idea.ticker}`}
            target="_blank"
            rel="noopener noreferrer"
            onClick={e => e.stopPropagation()}
            title={`Open ${idea.ticker} on TradingView Supercharts`}
          >
            {idea.ticker}
          </a>
          {idea.isNew && <span className="new-pip">NEW</span>}
        </div>
      </td>
      <td className="mono tnum">{fmtPrice(idea.underlying)}</td>
      <td><StratPill code={idea.strategy} /></td>
      <td className="mono tnum" style={{ color: "var(--text-2)" }}>{idea.exp}</td>
      <td><span className="dte-chip tnum">{idea.dte}d</span></td>
      <td className="mono tnum" style={{ color: "var(--text-2)" }}>
        {idea.shortK !== null ? `$${idea.shortK.toFixed(2)}` : "—"} <span style={{ color: "var(--text-3)" }}>/</span> {idea.longK !== null ? `$${idea.longK.toFixed(2)}` : "—"}
      </td>
      <td className={`mono tnum credit-cell ${isCredit ? "credit" : "debit"}`}>
        {fmtContractMoney(Math.abs(idea.credit))}
        <span className="credit-tag">{isCredit ? "CR" : "DB"}</span>
      </td>
      <td className="mono tnum" style={{ color: idea.maxProfit !== null ? "var(--text)" : "var(--text-3)" }}>
        {fmtContractMoney(idea.maxProfit)}
      </td>
      <td className="mono tnum" style={{ color: "var(--text-2)" }}>{fmtPct(idea.pop)}</td>
      <td className="mono tnum" style={{ color: "var(--text-2)" }}>{fmtPct(idea.ivRank)}</td>
      <td className="conf-cell">
        <div className="conf-bar">
          <div className="conf-fill" style={{ width: `${confPct}%` }} />
          <div className="conf-text">{confPct}%</div>
        </div>
        <ConfluenceTrendArrow idea={idea} />
      </td>
      <td className="spark-cell">
        {idea.spark && idea.spark.length >= 2
          ? <Sparkline data={idea.spark} />
          : <span className="spark-placeholder" title="Trend appears after a few days of scan history accumulate">—</span>}
      </td>
      <td className="track-cell">
        {onTrack && (
          <button
            className="track-btn"
            onClick={e => { e.stopPropagation(); onTrack(idea); }}
            title="Track this as an open position"
            aria-label="Track this position"
          >
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <path d="M12 4v16M4 12h16"/>
            </svg>
          </button>
        )}
      </td>
    </tr>
  );
}

/* ----- Sortable column header --------------------------------------------- */
// Renders a clickable <th> with an arrow indicator when active. Non-sortable
// columns (Strikes, Trend) just render the static label and don't fire onSort.
function SortableTH({ column, label, sort, onSort, sortable = true }) {
  if (!sortable || !onSort) return <th>{label}</th>;
  const isActive = sort && sort.column === column;
  const cls = `sortable ${isActive ? "is-sorted" : ""}`;
  return (
    <th className={cls} onClick={() => onSort(column)}>
      {label}
      <span className="sort-arrow" aria-hidden="true">
        {isActive ? (sort.direction === "asc" ? "↑" : "↓") : ""}
      </span>
    </th>
  );
}

/* ----- List view ----------------------------------------------------------- */
function IdeasTable({ ideas, selectedId, onSelect, sort, onSort, onTrackPosition }) {
  return (
    <div className="ideas-card">
      <table className="ideas-table">
        <thead>
          <tr>
            <SortableTH column="ticker"     label="Ticker"           sort={sort} onSort={onSort} />
            <SortableTH column="underlying" label="Price"            sort={sort} onSort={onSort} />
            <SortableTH column="strategy"   label="Strategy"         sort={sort} onSort={onSort} />
            <SortableTH column="exp"        label="Exp."             sort={sort} onSort={onSort} />
            <SortableTH column="dte"        label="DTE"              sort={sort} onSort={onSort} />
            <SortableTH                     label="Short / Long"     sortable={false} />
            <SortableTH column="credit"     label="Credit / Debit"   sort={sort} onSort={onSort} />
            <SortableTH column="maxProfit"  label="Max Profit"       sort={sort} onSort={onSort} />
            <SortableTH column="pop"        label="POP"              sort={sort} onSort={onSort} />
            <SortableTH column="ivRank"     label="IV Rank"          sort={sort} onSort={onSort} />
            <SortableTH column="confluence" label="Confluence"       sort={sort} onSort={onSort} />
            <SortableTH                     label="Trend"            sortable={false} />
            <SortableTH                     label=""                 sortable={false} />
          </tr>
        </thead>
        <tbody>
          {ideas.map(idea => (
            <IdeaRow
              key={idea.id} idea={idea}
              selected={idea.id === selectedId}
              onClick={() => onSelect(idea.id)}
              onTrack={onTrackPosition}
            />
          ))}
        </tbody>
      </table>
    </div>
  );
}

/* ----- Grouped view -------------------------------------------------------- */
function GroupedView({ ideas, selectedId, onSelect }) {
  const groups = useMemo(() => {
    const m = new Map();
    ideas.forEach(i => {
      if (!m.has(i.ticker)) m.set(i.ticker, []);
      m.get(i.ticker).push(i);
    });
    const arr = [...m.entries()].map(([ticker, list]) => ({
      ticker,
      list,
      best: Math.max(...list.map(x => x.confluence)),
      underlying: list[0].underlying,
      spark: list[0].spark,
    }));
    arr.sort((a, b) => b.best - a.best);
    return arr;
  }, [ideas]);

  return (
    <div>
      {groups.map(g => (
        <div className="group-card" key={g.ticker}>
          <div className="group-head">
            <a
              className="group-ticker ticker-link"
              href={`https://www.tradingview.com/chart/?symbol=${g.ticker}`}
              target="_blank"
              rel="noopener noreferrer"
              onClick={e => e.stopPropagation()}
              title={`Open ${g.ticker} on TradingView Supercharts`}
            >
              {g.ticker}
            </a>
            <div className="group-meta">
              <div className="group-price mono tnum">{fmtPrice(g.underlying)}</div>
              <div className="group-price-sub">{g.list.length} idea{g.list.length > 1 ? "s" : ""}</div>
            </div>
            <div className="group-spark"><Sparkline data={g.spark} width={120} height={36} /></div>
            <div className="group-best">
              top score
              <div className="group-best-num">{(g.best * 100).toFixed(1)}%</div>
            </div>
          </div>
          <div className="group-body">
            <table className="ideas-table">
              <tbody>
                {g.list.map(idea => (
                  <IdeaRow
                    key={idea.id} idea={idea}
                    selected={idea.id === selectedId}
                    onClick={() => onSelect(idea.id)}
                  />
                ))}
              </tbody>
            </table>
          </div>
        </div>
      ))}
    </div>
  );
}

/* ----- Confluence ring ----------------------------------------------------- */
function ConfRing({ value, size = 96, stroke = 8 }) {
  const r = (size - stroke) / 2;
  const c = 2 * Math.PI * r;
  const dash = c * value;
  return (
    <div className="ring" style={{ width: size, height: size }}>
      <svg viewBox={`0 0 ${size} ${size}`} width={size} height={size}>
        <circle cx={size/2} cy={size/2} r={r} fill="none" stroke="var(--surface-3)" strokeWidth={stroke} />
        <circle
          cx={size/2} cy={size/2} r={r}
          fill="none" stroke="var(--brand)" strokeWidth={stroke} strokeLinecap="round"
          strokeDasharray={`${dash} ${c}`}
        />
      </svg>
      <div className="ring-text tnum">{(value * 100).toFixed(0)}</div>
    </div>
  );
}

/* ----- Payoff Diagram ------------------------------------------------------ */
function PayoffDiagram({ idea, entrySpot }) {
  const W = 480, H = 180, padX = 26, padY = 18;
  const u = idea.underlying;
  const svgRef = useRef(null);
  // Hover/drag state — null means "not interacting; show spot only".
  // Set to a stock price (in dollars) when the user moves the mouse over
  // the chart, so we can render a movable cursor + per-contract P/L
  // readout at that price.
  const [hoverPrice, setHoverPrice] = useState(null);

  // Bail out gracefully if we don't have the inputs the curve math needs.
  // Happens when the primary leg's snapshot is missing (no underlying
  // price), or breakeven wasn't computed at strategy time.
  if (u === null || u === undefined || idea.breakeven === null || idea.breakeven === undefined) {
    return (
      <div className="payoff-card" style={{ padding: "32px 16px", textAlign: "center", color: "var(--text-3)", fontSize: 13 }}>
        Payoff curve unavailable — underlying price or breakeven missing.
      </div>
    );
  }

  // Compute strike spread, P/L curve.
  const strikes = [idea.longK, idea.shortK].filter(x => x !== null && x !== undefined);
  const minStrike = Math.min(...strikes, u) * 0.85;
  const maxStrike = Math.max(...strikes, u) * 1.15;
  const xRange = maxStrike - minStrike;

  // Payoff at price S (long position only — debit is negative credit field)
  const payoff = (S) => {
    const code = idea.strategy;
    const debit = -idea.credit; // positive for debit, negative for credit collected
    if (code === "LC") {
      return Math.max(0, S - idea.longK) - debit;
    } else if (code === "CSP") {
      // Short put: collect credit, lose if S < strike
      return idea.credit - Math.max(0, idea.longK - S);
    } else if (code === "PCS") {
      // Put credit spread: sell HIGHER-strike put (shortK), buy LOWER-strike
      // put (longK). dashboard.py emits shortK = sold strike (higher) and
      // longK = bought strike (lower), matching the design's mock data.
      // Payoff = credit collected − sold-leg intrinsic + bought-leg intrinsic.
      const shortP = Math.max(0, idea.shortK - S);
      const longP  = Math.max(0, idea.longK - S);
      return idea.credit - shortP + longP;
    } else if (code === "CDS") {
      // Buy lower-strike call (longK), sell higher-strike call (shortK)
      const longC  = Math.max(0, S - idea.longK);
      const shortC = Math.max(0, S - idea.shortK);
      return -(-idea.credit) + longC - shortC; // -(debit paid) + ...
    }
    return 0;
  };

  const N = 60;
  const samples = Array.from({ length: N }, (_, i) => {
    const S = minStrike + (xRange * i) / (N - 1);
    return { S, P: payoff(S) };
  });
  const Pmin = Math.min(...samples.map(s => s.P));
  const Pmax = Math.max(...samples.map(s => s.P));
  const Prange = (Pmax - Pmin) || 1;

  const xToPx = (S) => padX + ((S - minStrike) / xRange) * (W - 2 * padX);
  const yToPx = (P) => padY + (1 - (P - Pmin) / Prange) * (H - 2 * padY);
  const zeroY = yToPx(0);

  // Map a mouse-event clientX into a stock-price S, clamped to the chart's
  // visible price range so the readout never goes off-axis. Uses the SVG's
  // own coordinate system via getBoundingClientRect so it stays correct
  // regardless of CSS scaling.
  const handleMouseMove = (e) => {
    const svg = svgRef.current;
    if (!svg) return;
    const rect = svg.getBoundingClientRect();
    const localX = ((e.clientX - rect.left) / rect.width) * W;
    if (localX < padX || localX > W - padX) {
      setHoverPrice(null);
      return;
    }
    const S = minStrike + ((localX - padX) / (W - 2 * padX)) * xRange;
    setHoverPrice(Math.max(minStrike, Math.min(maxStrike, S)));
  };
  const handleMouseLeave = () => setHoverPrice(null);

  const cursor = hoverPrice !== null
    ? { S: hoverPrice, P: payoff(hoverPrice) }
    : null;

  // Build profit (>0) and loss (<0) regions as separate paths
  const dCurve = samples.map((s, i) =>
    `${i === 0 ? "M" : "L"} ${xToPx(s.S).toFixed(2)} ${yToPx(s.P).toFixed(2)}`
  ).join(" ");

  return (
    <div className="payoff-card">
      <svg className="payoff-svg" viewBox={`0 0 ${W} ${H}`} ref={svgRef}
           onMouseMove={handleMouseMove} onMouseLeave={handleMouseLeave}
           style={{ cursor: "crosshair" }}>
        <defs>
          <linearGradient id="profit-grad" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor="var(--bull)" stopOpacity="0.30" />
            <stop offset="100%" stopColor="var(--bull)" stopOpacity="0.02" />
          </linearGradient>
          <linearGradient id="loss-grad" x1="0" y1="0" x2="0" y2="1">
            <stop offset="0%" stopColor="var(--bear)" stopOpacity="0.02" />
            <stop offset="100%" stopColor="var(--bear)" stopOpacity="0.28" />
          </linearGradient>
          <clipPath id="profit-clip">
            <rect x="0" y="0" width={W} height={zeroY} />
          </clipPath>
          <clipPath id="loss-clip">
            <rect x="0" y={zeroY} width={W} height={H - zeroY} />
          </clipPath>
        </defs>

        {/* Zero line */}
        <line x1={padX} x2={W - padX} y1={zeroY} y2={zeroY} stroke="var(--border)" strokeDasharray="3 3" />

        {/* Filled regions (clip the curve area to above/below zero) */}
        <g clipPath="url(#profit-clip)">
          <path d={`${dCurve} L ${xToPx(samples[N-1].S)} ${zeroY} L ${xToPx(samples[0].S)} ${zeroY} Z`}
                fill="url(#profit-grad)" />
        </g>
        <g clipPath="url(#loss-clip)">
          <path d={`${dCurve} L ${xToPx(samples[N-1].S)} ${zeroY} L ${xToPx(samples[0].S)} ${zeroY} Z`}
                fill="url(#loss-grad)" />
        </g>

        {/* P/L curve */}
        <path d={dCurve} stroke="var(--text)" strokeWidth="1.6" fill="none" strokeLinejoin="round" />

        {/* Strike markers */}
        {strikes.map((K, i) => (
          <g key={i}>
            <line x1={xToPx(K)} x2={xToPx(K)} y1={padY} y2={H - padY} stroke="var(--text-3)" strokeDasharray="2 3" strokeOpacity="0.5" />
            <text x={xToPx(K)} y={padY - 4} fontSize="9" textAnchor="middle" fill="var(--text-2)" fontFamily="var(--font-mono)">${K}</text>
          </g>
        ))}

        {/* Entry-spot marker (positions only — when entrySpot prop is
            passed). Drawn first so the current-spot marker layers on top
            if both happen to be at the same price. Uses --text-3 (grey)
            so the BRAND-colored current spot stays the visual anchor. */}
        {entrySpot !== undefined && entrySpot !== null
          && entrySpot >= minStrike && entrySpot <= maxStrike && (
          <g pointerEvents="none">
            <line x1={xToPx(entrySpot)} x2={xToPx(entrySpot)}
                  y1={padY} y2={H - padY}
                  stroke="var(--text-3)" strokeWidth="1" strokeDasharray="3 2" />
            <circle cx={xToPx(entrySpot)} cy={zeroY} r="3"
                    fill="var(--surface)" stroke="var(--text-3)" strokeWidth="1.5" />
            <text x={xToPx(entrySpot)} y={padY - 4} fontSize="9"
                  textAnchor="middle" fill="var(--text-3)"
                  fontFamily="var(--font-mono)">entry ${entrySpot.toFixed(2)}</text>
          </g>
        )}

        {/* Underlying marker */}
        <line x1={xToPx(u)} x2={xToPx(u)} y1={padY} y2={H - padY} stroke="var(--brand)" strokeWidth="1.4" />
        <circle cx={xToPx(u)} cy={zeroY} r="3.5" fill="var(--brand)" stroke="var(--surface)" strokeWidth="2" />
        <text x={xToPx(u)} y={H - 4} fontSize="9" textAnchor="middle" fill="var(--brand)" fontFamily="var(--font-mono)" fontWeight="700">spot ${u.toFixed(2)}</text>

        {/* Breakeven */}
        <circle cx={xToPx(idea.breakeven)} cy={zeroY} r="3" fill="var(--surface)" stroke="var(--text)" strokeWidth="1.4" />

        {/* Hover cursor — vertical line + dot on the curve when the mouse
            is over the chart. The readout text floats above so it never
            overlaps the curve at extreme x positions. */}
        {cursor && (
          <g pointerEvents="none">
            <line x1={xToPx(cursor.S)} x2={xToPx(cursor.S)} y1={padY} y2={H - padY}
                  stroke="var(--text)" strokeWidth="1" strokeDasharray="2 3" strokeOpacity="0.6" />
            <circle cx={xToPx(cursor.S)} cy={yToPx(cursor.P)} r="4"
                    fill="var(--text)" stroke="var(--surface)" strokeWidth="2" />
          </g>
        )}
      </svg>
      <div className="payoff-legend">
        {cursor ? (
          <>
            <span className="payoff-readout-label">
              At ${cursor.S.toFixed(2)}:
            </span>
            <span className="payoff-readout-value" style={{ color: cursor.P >= 0 ? "var(--bull)" : "var(--bear)" }}>
              {cursor.P >= 0 ? "+" : "−"}${(Math.abs(cursor.P) * 100).toFixed(0)} per contract
            </span>
            <span className="payoff-readout-hint">
              hover to inspect; leave to reset
            </span>
          </>
        ) : (
          <>
            <span><span className="swatch" style={{background: "var(--bull)"}}/> Profit zone</span>
            <span><span className="swatch" style={{background: "var(--bear)"}}/> Loss zone</span>
            <span><span className="swatch" style={{background: "var(--brand)"}}/> Spot ${idea.underlying.toFixed(2)}</span>
            <span>○ Breakeven ${idea.breakeven.toFixed(2)}</span>
          </>
        )}
      </div>
    </div>
  );
}

/* ----- Underlying chart ---------------------------------------------------- */
function UnderlyingChart({ idea }) {
  const W = 480, H = 100, padX = 14, padY = 10;
  const data = idea.spark;
  const min = Math.min(...data, ...(idea.shortK ? [idea.shortK] : []), idea.longK);
  const max = Math.max(...data, ...(idea.shortK ? [idea.shortK] : []), idea.longK);
  const range = max - min || 1;
  const step = (W - 2 * padX) / (data.length - 1);
  const x = i => padX + i * step;
  const y = v => padY + (1 - (v - min) / range) * (H - 2 * padY);
  const d = data.map((v, i) => `${i === 0 ? "M" : "L"} ${x(i).toFixed(1)} ${y(v).toFixed(1)}`).join(" ");
  const isUp = data[data.length-1] >= data[0];
  return (
    <div className="under-card">
      <svg className="under-svg" viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none">
        {idea.longK && (
          <line x1={padX} x2={W - padX} y1={y(idea.longK)} y2={y(idea.longK)}
                stroke="var(--text-3)" strokeDasharray="2 3" />
        )}
        {idea.shortK && (
          <line x1={padX} x2={W - padX} y1={y(idea.shortK)} y2={y(idea.shortK)}
                stroke="var(--text-3)" strokeDasharray="2 3" />
        )}
        <path d={`${d} L ${x(data.length-1)} ${H-padY} L ${padX} ${H-padY} Z`}
              fill={isUp ? "color-mix(in oklab, var(--bull) 14%, transparent)" : "color-mix(in oklab, var(--bear) 14%, transparent)"} />
        <path d={d} stroke={isUp ? "var(--bull)" : "var(--bear)"} strokeWidth="1.6" fill="none" strokeLinejoin="round"/>
        <circle cx={x(data.length-1)} cy={y(data[data.length-1])} r="3" fill={isUp ? "var(--bull)" : "var(--bear)"} stroke="var(--surface-2)" strokeWidth="2"/>
      </svg>
    </div>
  );
}

/* ----- Drawer -------------------------------------------------------------- */
function Drawer({ idea, onClose }) {
  // Trap escape key
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    if (idea) window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [idea, onClose]);

  if (!idea) {
    return (
      <>
        <div className="drawer-backdrop" onClick={onClose} />
        <aside className="drawer" />
      </>
    );
  }
  const meta = window.STRATEGY_META[idea.strategy];
  const isCredit = idea.credit > 0;

  return (
    <>
      <div className="drawer-backdrop open" onClick={onClose} />
      <aside className="drawer open">
        <div className="drawer-head">
          <div>
            {/* Drawer header: full company name as the primary display.
                The ticker moves into the subtitle below where it still
                links out to TradingView. Company name falls back to the
                ticker when the universe lookup didn't find a match. */}
            <div className="drawer-title-row">
              <a
                className="drawer-company ticker-link"
                href={`https://www.tradingview.com/chart/?symbol=${idea.ticker}`}
                target="_blank"
                rel="noopener noreferrer"
                title={`Open ${idea.ticker} on TradingView Supercharts`}
              >
                {/* Prefer the side lookup (`window._COMPANIES`, set in data.js
                    as a fallback when the main IDEAS payload doesn't have
                    `company`), then the idea-level field, then the bare
                    ticker. Two fallback layers so this works whether the
                    drawer renders against an old or new data.js. */}
                {(window._COMPANIES && window._COMPANIES[idea.ticker]) || idea.company || idea.ticker}
              </a>
              <StratPill code={idea.strategy} />
              {idea.isNew && <span className="new-pip">NEW</span>}
            </div>
            <div className="drawer-meta">
              <span className="drawer-ticker-tag mono">{idea.ticker}</span>
              &nbsp;·&nbsp; {meta.name}
              &nbsp;·&nbsp; expires {idea.exp}
              &nbsp;·&nbsp; {idea.dte} days
            </div>
          </div>
          <button className="drawer-close" onClick={onClose} aria-label="Close">
            <Icon.Close />
          </button>
        </div>

        <div className="drawer-body">
          <div>
            <div className="ring-wrap">
              <ConfRing value={idea.confluence} />
              <div className="ring-info">
                <div className="ring-info-title">Confluence score</div>
                <div className="ring-info-sub">
                  Weighted blend of trend strength, IV regime, liquidity and recent option flow.
                </div>
              </div>
            </div>
          </div>

          <div>
            <div className="drawer-section-title">Contract</div>
            <div className="kv-grid">
              <div className="kv">
                <span className="kv-key">Strikes ({meta.leg})</span>
                <span className="kv-val">
                  {idea.shortK !== null && idea.shortK !== undefined ? `$${idea.shortK.toFixed(2)} / ` : ""}{fmtPrice(idea.longK)}
                </span>
              </div>
              <div className="kv">
                <span className="kv-key">{isCredit ? "Credit collected (1 contract)" : "Debit paid (1 contract)"}</span>
                <span className={`kv-val ${isCredit ? "up" : "down"}`}>
                  {fmtContractMoney(Math.abs(idea.credit))}
                </span>
              </div>
              <div className="kv">
                <span className="kv-key">Max profit (1 contract)</span>
                <span className="kv-val up">
                  {idea.maxProfit !== null ? fmtContractMoney(idea.maxProfit) : "Unlimited"}
                </span>
              </div>
              <div className="kv">
                <span className="kv-key">Max loss (1 contract)</span>
                <span className="kv-val down">{fmtContractMoney(idea.maxLoss)}</span>
              </div>
              <div className="kv">
                <span className="kv-key">Breakeven</span>
                <span className="kv-val">{fmtPrice(idea.breakeven)}</span>
              </div>
              <div className="kv">
                <span className="kv-key">POP</span>
                <span className="kv-val">{fmtPct(idea.pop)}</span>
              </div>
              <div className="kv">
                <span className="kv-key">IV Rank</span>
                <span className="kv-val">{fmtPct(idea.ivRank)}</span>
              </div>
              <div className="kv">
                <span className="kv-key">Underlying</span>
                <span className="kv-val">{fmtPrice(idea.underlying)}</span>
              </div>
            </div>
          </div>

          <div>
            <div className="drawer-section-title">Payoff at expiration</div>
            <PayoffDiagram idea={idea} />
          </div>

          <div>
            <div className="drawer-section-title">Underlying — last 12 sessions</div>
            <UnderlyingChart idea={idea} />
          </div>

          <div>
            <div className="drawer-section-title">Greeks</div>
            <div className="greeks-row">
              <div className="greek"><div className="greek-key">Δ Delta</div><div className="greek-val">{fmtNum(idea.delta, 2)}</div></div>
              <div className="greek"><div className="greek-key">Γ Gamma</div><div className="greek-val">{fmtNum(idea.gamma, 3)}</div></div>
              <div className="greek"><div className="greek-key">Θ Theta</div><div className="greek-val">{fmtNum(idea.theta, 2)}</div></div>
              <div className="greek"><div className="greek-key">ν Vega</div><div className="greek-val">{fmtNum(idea.vega, 2)}</div></div>
            </div>
          </div>

          <div>
            <div className="notes">
              <div className="notes-label">Why this scored {(idea.confluence*100).toFixed(1)}%</div>
              {idea.notes && <div style={{ marginBottom: 12 }}>{idea.notes}</div>}
              <ScoringBreakdown breakdown={idea.scoringBreakdown} confluence={idea.confluence} />
              <ConfluenceTrendSparkline idea={idea} />
            </div>
          </div>
        </div>
      </aside>
    </>
  );
}

/* ----- Scoring breakdown — confluence component bars ---------------------- */
// Renders the 5 confluence components (iv_alignment, technical_match,
// liquidity, spread_tightness, dte_sweet_spot) sorted by their weighted
// contribution to the final score. The bar fill represents the raw
// component score (0..1) so the eye can quickly spot which components
// are pulling the score up vs. holding it back. The right-side numbers
// show the weight (×%) and the absolute score for power users.
const COMPONENT_LABELS = {
  iv_alignment: "IV alignment",
  technical_match: "Technical match",
  liquidity: "Liquidity",
  spread_tightness: "Spread tightness",
  dte_sweet_spot: "DTE sweet spot",
};

function ScoringBreakdown({ breakdown, confluence }) {
  if (!breakdown || !breakdown.length) return null;
  return (
    <div className="scoring-grid">
      {breakdown.map(row => {
        const label = COMPONENT_LABELS[row.name] || row.name;
        const pct = Math.max(0, Math.min(1, row.score)) * 100;
        return (
          <div key={row.name} className="scoring-row" title={`${label}: score ${row.score.toFixed(2)} × weight ${(row.weight*100).toFixed(0)}% = ${(row.contribution*100).toFixed(1)} pts`}>
            <span className="scoring-name">{label}</span>
            <div className="scoring-bar-track">
              <div className="scoring-bar-fill" style={{ width: `${pct}%` }} />
            </div>
            <span className="scoring-score">{(row.score * 100).toFixed(0)}%</span>
            <span className="scoring-weight">·{(row.weight * 100).toFixed(0)}%</span>
          </div>
        );
      })}
      <div className="scoring-total">
        <span style={{ color: "var(--text-2)" }}>Weighted total</span>
        <strong>{(confluence * 100).toFixed(1)}%</strong>
      </div>
    </div>
  );
}

/* ============================================================================
   PositionDrawer — slide-in management view for an open position
   ----------------------------------------------------------------------------
   Different from the ideas Drawer because the questions are different:
   ideas drawer asks "is this a good trade?", positions drawer asks
   "should I close, hold, or roll?" We surface the data that supports
   that decision: current P/L, % to max, distance to breakeven, Greeks
   (especially theta — daily decay rate), payoff curve with both entry
   and current spot markers, and P/L trend since entry.
   ============================================================================*/

function PositionDrawer({ position, onClose, onCloseClick }) {
  // Escape key dismisses the drawer (matches ideas Drawer behavior).
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    if (position) window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [position, onClose]);

  if (!position) {
    return (
      <>
        <div className="drawer-backdrop" onClick={onClose} />
        <aside className="drawer" />
      </>
    );
  }
  const p = position;
  const meta = window.STRATEGY_META[p.strategy];
  const isCredit = p.isCredit;
  const costDollars = Math.abs(p.entryPrice) * 100 * p.quantity;
  // % to max: for credits, how close are we to keeping the full credit
  // (max profit). For debits, how close are we to losing the full debit
  // (max loss). The dominant management heuristic per tastytrade
  // methodology: close credit trades at 50% of max profit; manage debit
  // trades at 50% loss as a defensive stop.
  const maxProfit = p.maxProfit;
  const maxLoss = p.maxLoss;
  const pctToMaxProfit = (maxProfit !== null && maxProfit > 0 && p.pnlPerShare !== null)
    ? Math.max(0, Math.min(1, p.pnlPerShare / maxProfit))
    : null;
  const pctToMaxLoss = (maxLoss !== null && maxLoss < 0 && p.pnlPerShare !== null && p.pnlPerShare < 0)
    ? Math.max(0, Math.min(1, p.pnlPerShare / maxLoss))
    : null;
  // Breakeven distance from current spot, signed positive when in our
  // favor (above BE for LC/CDS, below BE for CSP/PCS).
  const beDistancePct = (p.breakeven !== null && p.underlying)
    ? (isCredit
        ? (p.underlying - p.breakeven) / p.underlying
        : (p.underlying - p.breakeven) / p.underlying)
    : null;
  const dteWarn = p.dte !== null && p.dte <= 21;  // tastytrade: manage at 21 DTE
  const dteCritical = p.dte !== null && p.dte <= 7;
  // Idea-shape adapter so we can reuse PayoffDiagram with position data.
  const ideaShape = {
    underlying: p.underlying,
    breakeven: p.breakeven,
    longK: p.longK,
    shortK: p.shortK,
    credit: p.entryPrice,            // already signed correctly
    strategy: p.strategy,
  };

  return (
    <>
      <div className="drawer-backdrop open" onClick={onClose} />
      <aside className="drawer open">
        <div className="drawer-head">
          <div>
            <div className="drawer-title-row">
              <a
                className="drawer-company ticker-link"
                href={`https://www.tradingview.com/chart/?symbol=${p.ticker}`}
                target="_blank"
                rel="noopener noreferrer"
                title={`Open ${p.ticker} on TradingView Supercharts`}
              >
                {p.ticker}
              </a>
              <StratPill code={p.strategy} />
            </div>
            <div className="drawer-meta">
              {meta.name} · {p.quantity} contract{p.quantity === 1 ? "" : "s"}
              &nbsp;·&nbsp; opened {p.dateOpened}
              &nbsp;·&nbsp; {p.daysHeld}d held
              &nbsp;·&nbsp; <span className={dteCritical ? "dte-critical" : dteWarn ? "dte-warn" : ""}>
                {p.dte}d to expiration
              </span>
            </div>
          </div>
          <button className="drawer-close" onClick={onClose} aria-label="Close">
            <Icon.Close />
          </button>
        </div>

        <div className="drawer-body">
          {/* Position summary — entry vs current vs P/L */}
          <div>
            <div className="drawer-section-title">Position</div>
            <div className="kv-grid">
              <div className="kv">
                <span className="kv-key">{isCredit ? "Credit collected" : "Debit paid"}</span>
                <span className={`kv-val ${isCredit ? "up" : "down"}`}>
                  ${Math.abs(p.entryPrice).toFixed(2)} <span className="kv-unit">/share</span>
                </span>
              </div>
              <div className="kv">
                <span className="kv-key">Total {isCredit ? "credit" : "cost"}</span>
                <span className="kv-val">${costDollars.toFixed(0)}</span>
              </div>
              <div className="kv">
                <span className="kv-key">Current mid</span>
                <span className="kv-val">
                  {p.currentMid !== null ? `$${Math.abs(p.currentMid).toFixed(2)}` : "—"}
                </span>
              </div>
              <div className="kv">
                <span className="kv-key">Total P/L</span>
                <span className={`kv-val ${p.pnlTotal !== null && p.pnlTotal >= 0 ? "up" : "down"}`}>
                  {p.pnlTotal === null ? "—" :
                    `${p.pnlTotal >= 0 ? "+" : "−"}$${Math.abs(p.pnlTotal).toFixed(0)}`}
                </span>
              </div>
              <div className="kv">
                <span className="kv-key">Strikes</span>
                <span className="kv-val">
                  {p.shortK !== null && p.shortK !== undefined ? `$${p.shortK.toFixed(2)} / ` : ""}
                  {p.longK !== null && p.longK !== undefined ? `$${p.longK.toFixed(2)}` : "—"}
                </span>
              </div>
              <div className="kv">
                <span className="kv-key">Breakeven</span>
                <span className="kv-val">${p.breakeven !== null ? p.breakeven.toFixed(2) : "—"}</span>
              </div>
              <div className="kv">
                <span className="kv-key">Underlying</span>
                <span className="kv-val">${p.underlying !== null ? p.underlying.toFixed(2) : "—"}</span>
              </div>
              <div className="kv">
                <span className="kv-key">Entry spot</span>
                <span className="kv-val">${p.entrySpot !== null ? p.entrySpot.toFixed(2) : "—"}</span>
              </div>
            </div>
          </div>

          {/* Decision context — health gauge + heuristics */}
          <div>
            <div className="drawer-section-title">Health</div>
            <PositionHealth p={p}
                            pctToMaxProfit={pctToMaxProfit}
                            pctToMaxLoss={pctToMaxLoss}
                            beDistancePct={beDistancePct} />
          </div>

          {/* Payoff at expiration — reuses ideas drawer component, with
              the entry-spot marker added (Pass 2 extension) so the
              operator can see how far the stock has drifted since the
              trade was opened. */}
          <div>
            <div className="drawer-section-title">Payoff at expiration</div>
            <PayoffDiagram idea={ideaShape} entrySpot={p.entrySpot} />
          </div>

          {/* P/L trend since entry — sparkline reading position_snapshots.
              Empty until at least 2 datapoints exist (a fresh position
              with one snapshot has nothing to draw yet). */}
          <PositionPnlTrend trend={p.pnlTrend || []} currentPnl={p.pnlTotal} />

          {/* Greeks — net per-position. Theta is the most actionable:
              positive = earning daily decay; negative = bleeding it. */}
          <div>
            <div className="drawer-section-title">Greeks (net)</div>
            <div className="greeks-row">
              <div className="greek">
                <div className="greek-key">Δ Delta</div>
                <div className="greek-val">{fmtNum(p.delta, 2)}</div>
              </div>
              <div className="greek">
                <div className="greek-key">Γ Gamma</div>
                <div className="greek-val">{fmtNum(p.gamma, 3)}</div>
              </div>
              <div className="greek">
                <div className="greek-key">Θ Theta /day</div>
                <div className={`greek-val ${p.theta !== null && p.theta >= 0 ? "positive" : "negative"}`}>
                  {p.theta === null ? "—" :
                    `${p.theta >= 0 ? "+" : "−"}$${Math.abs(p.theta * 100 * p.quantity).toFixed(0)}`}
                </div>
              </div>
              <div className="greek">
                <div className="greek-key">ν Vega</div>
                <div className="greek-val">{fmtNum(p.vega, 2)}</div>
              </div>
            </div>
            <div className="greeks-hint">
              {p.theta !== null && p.theta >= 0 && "Theta in your favor — collecting decay daily."}
              {p.theta !== null && p.theta < 0 && "Theta against you — paying decay daily. Time decay accelerates near expiration."}
            </div>
          </div>

          {/* Mark-as-closed CTA */}
          <div className="position-drawer-actions">
            <button className="primary danger" onClick={() => onCloseClick(p.id)}>
              Mark as closed
            </button>
          </div>
        </div>
      </aside>
    </>
  );
}

/* ----- Position health — three small gauges ------------------------------- */
function PositionHealth({ p, pctToMaxProfit, pctToMaxLoss, beDistancePct }) {
  const isCredit = p.isCredit;
  // Primary gauge depends on strategy:
  // - Credit: how close to max profit (target close at 50%, per
  //   tastytrade's "manage winners" rule)
  // - Debit: how close to max loss (defensive stop)
  const primary = isCredit
    ? { label: "% to max profit", value: pctToMaxProfit, target: 0.5,
        targetLabel: "close zone (50%+)", color: "var(--bull)" }
    : { label: "% to max loss", value: pctToMaxLoss, target: 0.5,
        targetLabel: "stop zone (50%+)", color: "var(--bear)" };
  const pct = primary.value !== null ? primary.value * 100 : null;
  const inTarget = pct !== null && pct >= primary.target * 100;
  return (
    <div className="health-block">
      <div className="health-row">
        <div className="health-label">{primary.label}</div>
        <div className="health-bar-track">
          <div className="health-bar-fill" style={{ width: `${pct || 0}%`, background: primary.color }} />
          <div className="health-bar-target" style={{ left: `${primary.target * 100}%` }}
               title={primary.targetLabel} />
        </div>
        <div className="health-value mono tnum">
          {pct === null ? "—" : `${pct.toFixed(0)}%`}
        </div>
      </div>
      {inTarget && (
        <div className="health-flag" style={{ color: primary.color }}>
          {isCredit ? "→ Consider closing — at or past 50% max profit" :
                      "→ Consider stop — at or past 50% max loss"}
        </div>
      )}
      <div className="health-row">
        <div className="health-label">Days to expiration</div>
        <div className="health-bar-track">
          {/* Bar fills from 60 days down to 0 — DTE 60 = full bar, DTE 0 = empty. */}
          <div className="health-bar-fill"
               style={{
                 width: `${Math.max(0, Math.min(100, (p.dte / 60) * 100))}%`,
                 background: p.dte <= 7 ? "var(--bear)" : p.dte <= 21 ? "var(--warn)" : "var(--bull)"
               }} />
          {/* 21 DTE marker — tastytrade's manage threshold */}
          <div className="health-bar-target" style={{ left: `${(21 / 60) * 100}%` }}
               title="21 DTE — tastytrade manage threshold" />
        </div>
        <div className="health-value mono tnum">{p.dte}d</div>
      </div>
      {p.dte !== null && p.dte <= 21 && (
        <div className="health-flag" style={{ color: p.dte <= 7 ? "var(--bear)" : "var(--warn)" }}>
          → {p.dte <= 7 ? "≤7 DTE — gamma risk is high; close or roll" :
                          "≤21 DTE — tastytrade roll/close threshold"}
        </div>
      )}
      <div className="health-row">
        <div className="health-label">Distance to breakeven</div>
        <div className="health-distance">
          {beDistancePct === null ? (
            <span className="mono tnum">—</span>
          ) : (
            <span className={`mono tnum ${beDistancePct >= 0 ? "positive" : "negative"}`}>
              {beDistancePct >= 0 ? "+" : ""}{(beDistancePct * 100).toFixed(1)}%
              <span className="health-distance-hint">
                {" "}
                {beDistancePct >= 0
                  ? (isCredit ? "above BE — winning" : "above BE — winning")
                  : (isCredit ? "below BE — losing" : "below BE — needs +" + (-beDistancePct * 100).toFixed(1) + "%")}
              </span>
            </span>
          )}
        </div>
      </div>
    </div>
  );
}

/* ----- P/L trend sparkline (since entry) --------------------------------- */
function PositionPnlTrend({ trend, currentPnl }) {
  // Need at least 2 points (or 1 historical + current) to draw.
  const points = [...trend];
  if (currentPnl !== null && currentPnl !== undefined) {
    points.push({ capturedAt: null, pnlPerContract: currentPnl });
  }
  if (points.length < 2) return null;
  const W = 480, H = 60, pad = 4;
  const values = points.map(p => p.pnlPerContract);
  const min = Math.min(...values, 0);  // include zero baseline
  const max = Math.max(...values, 0);
  const range = (max - min) || 1;
  const step = (W - 2 * pad) / (points.length - 1);
  const pathD = points.map((p, i) => {
    const x = pad + i * step;
    const y = pad + (1 - (p.pnlPerContract - min) / range) * (H - 2 * pad);
    return `${i === 0 ? "M" : "L"} ${x.toFixed(1)} ${y.toFixed(1)}`;
  }).join(" ");
  const zeroY = pad + (1 - (0 - min) / range) * (H - 2 * pad);
  const isUp = points[points.length - 1].pnlPerContract >= points[0].pnlPerContract;
  const stroke = isUp ? "var(--bull)" : "var(--bear)";
  return (
    <div>
      <div className="drawer-section-title">P/L trend since entry</div>
      <div className="payoff-card" style={{ padding: 12 }}>
        <svg viewBox={`0 0 ${W} ${H}`} className="conf-trend-spark-svg" style={{ height: H }}>
          <line x1={pad} x2={W - pad} y1={zeroY} y2={zeroY}
                stroke="var(--border)" strokeDasharray="2 2" />
          <path d={pathD} stroke={stroke} strokeWidth="1.6" fill="none" strokeLinejoin="round" />
        </svg>
        <div className="payoff-legend">
          <span style={{ color: "var(--text-3)" }}>{points.length} datapoints</span>
          <span style={{ marginLeft: "auto" }} className="mono tnum">
            min ${min.toFixed(0)} / max ${max.toFixed(0)}
          </span>
        </div>
      </div>
    </div>
  );
}

/* ============================================================================
   Positions tab — manual portfolio tracker
   ----------------------------------------------------------------------------
   The operator places trades on Robinhood (or another broker) and submits
   the fill data here. The dashboard marks each open position to market on
   every refresh by looking up the contract in the latest option_snapshots.

   PositionsView         — top-level layout (hero summary + table)
   PositionsHero         — three summary cards (open count + total P/L + best)
   PositionsTable        — list of open positions with current vs entry
   ClosedHistory         — collapsible last-N closed positions with realized P/L
   AddPositionModal      — strategy-aware form for creating a position
   ClosePositionModal    — exit price / date prompt
   ============================================================================*/

function PositionsView({ onAddClick, onCloseClick, onSelectPosition }) {
  const positions = window.POSITIONS || [];
  const open = positions.filter(p => p.status === "open");
  return (
    <>
      <PositionsHero open={open} />
      <div className="section-head positions-head">
        <div>
          <div className="section-title">Open positions</div>
          <div className="section-sub">
            {open.length === 0
              ? "no open positions yet — click + Add position or use the Track icon on any idea"
              : `${open.length} open · click any row for full management view · marked to market every scan`}
          </div>
        </div>
        <button className="add-position-btn" onClick={onAddClick}>
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M12 5v14M5 12h14" />
          </svg>
          Add position
        </button>
      </div>
      {open.length > 0 && <PositionsTable positions={open}
                                          onCloseClick={onCloseClick}
                                          onSelectPosition={onSelectPosition} />}
      {/* Closed trades live in the History tab now (third top-level tab).
          Phase 4k moved them out of PositionsView so this view is a clean
          "what am I currently managing" surface. */}
    </>
  );
}

function PositionsHero({ open }) {
  const totalPnl = open.reduce((sum, p) => sum + (p.pnlTotal || 0), 0);
  const totalCapital = open.reduce((sum, p) => {
    // Capital at risk = max loss × qty for spreads/LC; for credit-only
    // (CSP) treat the cash-secured collateral as the strike × qty × 100.
    if (p.maxLoss !== null && p.maxLoss !== undefined) {
      return sum + Math.abs(p.maxLoss) * 100 * p.quantity;
    }
    return sum;
  }, 0);
  // Best / worst of the open set, by % return.
  const ranked = open
    .filter(p => p.pnlPerShare !== null && p.entryPrice)
    .map(p => ({ ...p, pct: p.pnlPerShare / Math.abs(p.entryPrice) }))
    .sort((a, b) => b.pct - a.pct);
  const best = ranked[0];
  const worst = ranked[ranked.length - 1];
  return (
    <div className="hero positions-hero">
      <div className="card">
        <div className="card-label"><span className="card-label-dot" /> Open positions</div>
        <div className="summary-stat">
          <div className="summary-num tnum">{open.length}</div>
          <div className="summary-unit">positions</div>
        </div>
        <div className="summary-sub mono">${totalCapital.toFixed(0)} capital at risk</div>
      </div>
      <div className="card">
        <div className="card-label"><span className="card-label-dot" /> Unrealized P/L</div>
        <div className="summary-stat">
          <div className={`summary-num tnum ${totalPnl >= 0 ? "positive" : "negative"}`}>
            {totalPnl >= 0 ? "+" : "−"}${Math.abs(totalPnl).toFixed(0)}
          </div>
          <div className="summary-unit">across {open.length} positions</div>
        </div>
      </div>
      <div className="card">
        <div className="card-label"><span className="card-label-dot" /> Movers</div>
        {best && (
          <div className="diff-row">
            <div>
              <div className="diff-label">Best</div>
              <div className="positions-mover-line">
                <span className="mono">{best.ticker}</span>
                <span className={`strat-pill ${best.strategy}`}>{best.strategy}</span>
                <span className={`positions-mover-pct ${best.pct >= 0 ? "positive" : "negative"} mono tnum`}>
                  {best.pct >= 0 ? "+" : ""}{(best.pct * 100).toFixed(1)}%
                </span>
              </div>
            </div>
          </div>
        )}
        {worst && worst !== best && (
          <div className="diff-row">
            <div>
              <div className="diff-label">Worst</div>
              <div className="positions-mover-line">
                <span className="mono">{worst.ticker}</span>
                <span className={`strat-pill ${worst.strategy}`}>{worst.strategy}</span>
                <span className={`positions-mover-pct ${worst.pct >= 0 ? "positive" : "negative"} mono tnum`}>
                  {worst.pct >= 0 ? "+" : ""}{(worst.pct * 100).toFixed(1)}%
                </span>
              </div>
            </div>
          </div>
        )}
        {open.length === 0 && (
          <div className="diff-label" style={{ marginTop: 14 }}>No open positions yet</div>
        )}
      </div>
    </div>
  );
}

function PositionsTable({ positions, onCloseClick, onSelectPosition }) {
  return (
    <div className="ideas-card">
      <table className="ideas-table positions-table">
        <thead>
          <tr>
            <th>Ticker</th>
            <th>Strategy</th>
            <th>Strikes</th>
            <th>Exp</th>
            <th>DTE</th>
            <th>Qty</th>
            <th>Entry</th>
            <th title="Total cash debited or credited at fill (entry × 100 × qty)">Cost</th>
            <th>Current</th>
            <th>P/L $</th>
            <th>P/L %</th>
            <th></th>
          </tr>
        </thead>
        <tbody>
          {positions.map(p => <PositionRow key={p.id} p={p}
                                            onCloseClick={onCloseClick}
                                            onSelectPosition={onSelectPosition} />)}
        </tbody>
      </table>
    </div>
  );
}

function PositionRow({ p, onCloseClick, onSelectPosition }) {
  const isCredit = p.isCredit;
  // Display ALWAYS as positive numbers (premium magnitudes); the sign of
  // the P/L is what tells you direction. Operator reads "I paid $8.40"
  // not "I had a -8.40 cash flow."
  const entryDisplay = `$${Math.abs(p.entryPrice).toFixed(2)}`;
  const currentDisplay = p.currentMid !== null
    ? `$${Math.abs(p.currentMid).toFixed(2)}`
    : "—";
  // Cost = total cash debited (paid) or credited (received) at fill.
  // Per-share entry × 100 × qty. Tells the operator at a glance how
  // much money is committed to this position.
  const costDollars = Math.abs(p.entryPrice) * 100 * p.quantity;
  const costDisplay = `$${costDollars.toFixed(0)} ${isCredit ? "cr" : "db"}`;
  const pnlPct = p.pnlPerShare !== null && p.entryPrice
    ? (p.pnlPerShare / Math.abs(p.entryPrice)) * 100
    : null;
  const dteWarn = p.dte !== null && p.dte <= 7;
  return (
    <tr
      className={dteWarn ? "is-near-exp" : ""}
      onClick={onSelectPosition ? () => onSelectPosition(p.id) : undefined}
      style={onSelectPosition ? { cursor: "pointer" } : undefined}
    >
      <td>
        <a
          className="ticker-symbol mono ticker-link"
          href={`https://www.tradingview.com/chart/?symbol=${p.ticker}`}
          target="_blank"
          rel="noopener noreferrer"
          onClick={e => e.stopPropagation()}
          title={`Open ${p.ticker} on TradingView Supercharts`}
        >
          {p.ticker}
        </a>
      </td>
      <td><span className={`strat-pill ${p.strategy}`}>{p.strategy}</span></td>
      <td className="mono tnum" style={{ color: "var(--text-2)" }}>
        {p.shortK !== null && p.shortK !== undefined ? `$${p.shortK.toFixed(2)} / ` : ""}
        {p.longK !== null && p.longK !== undefined ? `$${p.longK.toFixed(2)}` : "—"}
      </td>
      <td className="mono tnum" style={{ color: "var(--text-2)" }}>{p.expiration}</td>
      <td><span className={`dte-chip tnum ${dteWarn ? "warn" : ""}`}>{p.dte !== null ? `${p.dte}d` : "—"}</span></td>
      <td className="mono tnum">{p.quantity}</td>
      <td className="mono tnum">{entryDisplay} {isCredit ? "cr" : "db"}</td>
      <td className={`mono tnum cost-cell ${isCredit ? "credit" : "debit"}`}
          title={`${isCredit ? "Cash credited" : "Cash debited"} at fill: ${entryDisplay} × 100 × ${p.quantity} = $${costDollars.toFixed(0)}`}>
        {costDisplay}
      </td>
      <td className="mono tnum">{currentDisplay}</td>
      <td className={`mono tnum ${p.pnlTotal !== null && p.pnlTotal >= 0 ? "positive" : "negative"}`}>
        {p.pnlTotal === null ? "—" : `${p.pnlTotal >= 0 ? "+" : "−"}$${Math.abs(p.pnlTotal).toFixed(0)}`}
      </td>
      <td className={`mono tnum ${pnlPct !== null && pnlPct >= 0 ? "positive" : "negative"}`}>
        {pnlPct === null ? "—" : `${pnlPct >= 0 ? "+" : ""}${pnlPct.toFixed(1)}%`}
      </td>
      <td className="track-cell">
        <button
          className="track-btn"
          onClick={e => { e.stopPropagation(); onCloseClick(p.id); }}
          title="Mark this position as closed (records exit price + realized P/L)"
        >
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M6 6l12 12M18 6L6 18"/>
          </svg>
        </button>
      </td>
    </tr>
  );
}

function ClosedHistory({ positions }) {
  const [expanded, setExpanded] = useState(false);
  if (positions.length === 0) return null;
  return (
    <div className="closed-history">
      <button className="closed-history-toggle" onClick={() => setExpanded(!expanded)}>
        {expanded ? "▾" : "▸"} Closed history ({positions.length})
      </button>
      {expanded && (
        <div className="ideas-card">
          <table className="ideas-table positions-table">
            <thead>
              <tr>
                <th>Ticker</th><th>Strategy</th><th>Opened</th><th>Closed</th>
                <th>Entry</th><th>Exit</th><th>Realized P/L</th><th>Return %</th>
              </tr>
            </thead>
            <tbody>
              {positions.map(p => {
                const pct = p.pnlPerShare !== null && p.entryPrice
                  ? (p.pnlPerShare / Math.abs(p.entryPrice)) * 100 : null;
                return (
                  <tr key={p.id}>
                    <td className="mono">{p.ticker}</td>
                    <td><span className={`strat-pill ${p.strategy}`}>{p.strategy}</span></td>
                    <td className="mono tnum" style={{ color: "var(--text-2)" }}>{p.dateOpened}</td>
                    <td className="mono tnum" style={{ color: "var(--text-2)" }}>{p.dateClosed}</td>
                    <td className="mono tnum">${Math.abs(p.entryPrice).toFixed(2)}</td>
                    <td className="mono tnum">{p.exitPrice !== null ? `$${Math.abs(p.exitPrice).toFixed(2)}` : "—"}</td>
                    <td className={`mono tnum ${p.pnlTotal !== null && p.pnlTotal >= 0 ? "positive" : "negative"}`}>
                      {p.pnlTotal === null ? "—" : `${p.pnlTotal >= 0 ? "+" : "−"}$${Math.abs(p.pnlTotal).toFixed(0)}`}
                    </td>
                    <td className={`mono tnum ${pct !== null && pct >= 0 ? "positive" : "negative"}`}>
                      {pct === null ? "—" : `${pct >= 0 ? "+" : ""}${pct.toFixed(1)}%`}
                    </td>
                  </tr>
                );
              })}
            </tbody>
          </table>
        </div>
      )}
    </div>
  );
}

/* ----- Add-position modal ---------------------------------------------------
 * Strategy-aware form. Renders the right strike inputs based on which
 * strategy is selected. Validates client-side and POSTs to /positions;
 * on success reloads the page (data.js refreshes with the new entry).
 *
 * Sign convention for entry_price: stored as SIGNED net cash flow.
 *   LC, CDS (debit):  negative (cash out at fill)
 *   CSP, PCS (credit): positive (cash in at fill)
 * The form takes a positive magnitude from the user and applies the
 * sign automatically based on strategy — operators don't think in
 * negatives. ``prefill`` from a Track-button click already has the
 * signed value (idea.credit).
 * --------------------------------------------------------------------------- */
function AddPositionModal({ prefill, onClose }) {
  const init = prefill || {};
  const [ticker, setTicker] = useState(init.ticker || "");
  const [strategy, setStrategy] = useState(init.strategy || "lc");
  const [longK, setLongK] = useState(init.longK ?? "");
  const [shortK, setShortK] = useState(init.shortK ?? "");
  const [expiration, setExpiration] = useState(init.expiration || "");
  const [quantity, setQuantity] = useState(init.quantity || 1);
  // Entry price as a positive magnitude. Signed value computed at submit.
  const [entryMagnitude, setEntryMagnitude] = useState(
    init.entry_price !== undefined ? Math.abs(init.entry_price).toFixed(2) : ""
  );
  const [dateOpened, setDateOpened] = useState(
    init.date_opened || new Date().toISOString().slice(0, 10)
  );
  const [submitting, setSubmitting] = useState(false);
  const [error, setError] = useState(null);

  const isCredit = strategy === "pcs" || strategy === "csp";
  const showLongK = strategy === "lc" || strategy === "cds" || strategy === "pcs";
  const showShortK = strategy === "cds" || strategy === "pcs" || strategy === "csp";

  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  const submit = async (e) => {
    e.preventDefault();
    setError(null);
    setSubmitting(true);
    try {
      const mag = parseFloat(entryMagnitude);
      if (isNaN(mag) || mag <= 0) throw new Error("Entry price must be a positive number");
      const body = {
        ticker: ticker.trim().toUpperCase(),
        strategy,
        long_strike: showLongK && longK !== "" ? parseFloat(longK) : null,
        short_strike: showShortK && shortK !== "" ? parseFloat(shortK) : null,
        expiration,
        quantity: parseInt(quantity, 10),
        // Sign-applied: + for credit strategies, − for debit. Stored
        // this way so dashboard's mark-to-market formula is uniform.
        entry_price: isCredit ? mag : -mag,
        date_opened: dateOpened,
      };
      const r = await fetch("/positions", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      if (!r.ok) {
        const msg = await r.text();
        throw new Error(`POST /positions → ${r.status}: ${msg}`);
      }
      // Reload to pick up fresh data.js with the new position included.
      window.location.reload();
    } catch (err) {
      setError(err.message);
      setSubmitting(false);
    }
  };

  return (
    <>
      <div className="drawer-backdrop open" onClick={onClose} style={{ zIndex: 70 }} />
      <div className="position-modal" role="dialog" aria-label="Add position">
        <div className="position-modal-head">
          <div>
            <div className="position-modal-title">Add position</div>
            <div className="position-modal-sub">
              Manual entry — fill values come from your broker (Robinhood, etc.)
            </div>
          </div>
          <button className="drawer-close" onClick={onClose} aria-label="Close">
            <Icon.Close />
          </button>
        </div>
        <form className="position-form" onSubmit={submit}>
          <label className="form-row">
            <span>Ticker</span>
            <input type="text" value={ticker} onChange={e => setTicker(e.target.value)}
                   placeholder="MP" required maxLength={10} autoFocus />
          </label>
          <label className="form-row">
            <span>Strategy</span>
            <select value={strategy} onChange={e => setStrategy(e.target.value)}>
              <option value="lc">LC — Long Call (debit)</option>
              <option value="cds">CDS — Call Debit Spread (debit)</option>
              <option value="csp">CSP — Cash-Secured Put (credit)</option>
              <option value="pcs">PCS — Put Credit Spread (credit)</option>
            </select>
          </label>
          {showLongK && (
            <label className="form-row">
              <span>{strategy === "lc" ? "Strike" : "Long strike"}</span>
              <input type="number" step="0.5" value={longK} onChange={e => setLongK(e.target.value)} required />
            </label>
          )}
          {showShortK && (
            <label className="form-row">
              <span>{strategy === "csp" ? "Strike" : "Short strike"}</span>
              <input type="number" step="0.5" value={shortK} onChange={e => setShortK(e.target.value)} required />
            </label>
          )}
          <label className="form-row">
            <span>Expiration</span>
            <input type="date" value={expiration} onChange={e => setExpiration(e.target.value)} required />
          </label>
          <label className="form-row">
            <span>Quantity (contracts)</span>
            <input type="number" min="1" step="1" value={quantity} onChange={e => setQuantity(e.target.value)} required />
          </label>
          <label className="form-row">
            <span>{isCredit ? "Credit collected" : "Debit paid"} (per share)</span>
            <input type="number" step="0.01" min="0" value={entryMagnitude}
                   onChange={e => setEntryMagnitude(e.target.value)}
                   placeholder="8.40" required />
          </label>
          <label className="form-row">
            <span>Date opened</span>
            <input type="date" value={dateOpened} onChange={e => setDateOpened(e.target.value)} required />
          </label>
          {error && <div className="form-error">{error}</div>}
          <div className="form-actions">
            <button type="button" onClick={onClose} disabled={submitting}>Cancel</button>
            <button type="submit" className="primary" disabled={submitting}>
              {submitting ? "Saving..." : "Track position →"}
            </button>
          </div>
        </form>
      </div>
    </>
  );
}

function ClosePositionModal({ positionId, onClose }) {
  const position = (window.POSITIONS || []).find(p => p.id === positionId);
  const [exitMagnitude, setExitMagnitude] = useState(
    position && position.currentMid !== null ? Math.abs(position.currentMid).toFixed(2) : ""
  );
  const [dateClosed, setDateClosed] = useState(new Date().toISOString().slice(0, 10));
  const [submitting, setSubmitting] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  if (!position) return null;
  const isCredit = position.isCredit;

  const submit = async (e) => {
    e.preventDefault();
    setError(null);
    setSubmitting(true);
    try {
      const mag = parseFloat(exitMagnitude);
      if (isNaN(mag) || mag < 0) throw new Error("Exit price must be ≥ 0");
      // exit_price uses the same sign convention as currentMid: signed
      // close cash flow with opposite sign to entry direction.
      // For LC/CDS (debit entry, signed -): exit cash IN = +mag
      // For CSP/PCS (credit entry, signed +): exit cash OUT = -mag
      const signedExit = isCredit ? -mag : mag;
      const r = await fetch(`/positions/${positionId}/close`, {
        method: "PATCH",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ exit_price: signedExit, date_closed: dateClosed }),
      });
      if (!r.ok) {
        const msg = await r.text();
        throw new Error(`PATCH /positions/${positionId}/close → ${r.status}: ${msg}`);
      }
      window.location.reload();
    } catch (err) {
      setError(err.message);
      setSubmitting(false);
    }
  };

  return (
    <>
      <div className="drawer-backdrop open" onClick={onClose} style={{ zIndex: 70 }} />
      <div className="position-modal" role="dialog" aria-label="Close position">
        <div className="position-modal-head">
          <div>
            <div className="position-modal-title">Close {position.ticker} {position.strategy}</div>
            <div className="position-modal-sub">
              Record realized exit. Position moves to closed history.
            </div>
          </div>
          <button className="drawer-close" onClick={onClose} aria-label="Close">
            <Icon.Close />
          </button>
        </div>
        <form className="position-form" onSubmit={submit}>
          <label className="form-row">
            <span>{isCredit ? "Cost to close (BTC)" : "Sell-to-close credit"} (per share)</span>
            <input type="number" step="0.01" min="0" value={exitMagnitude}
                   onChange={e => setExitMagnitude(e.target.value)}
                   placeholder="0.00" required autoFocus />
          </label>
          <label className="form-row">
            <span>Date closed</span>
            <input type="date" value={dateClosed} onChange={e => setDateClosed(e.target.value)} required />
          </label>
          {error && <div className="form-error">{error}</div>}
          <div className="form-actions">
            <button type="button" onClick={onClose} disabled={submitting}>Cancel</button>
            <button type="submit" className="primary" disabled={submitting}>
              {submitting ? "Saving..." : "Close position"}
            </button>
          </div>
        </form>
      </div>
    </>
  );
}

/* ============================================================================
   History tab — closed trades + lifetime performance
   ----------------------------------------------------------------------------
   The third top-level tab. Shows realized P/L only (no marking-to-market) —
   each trade's outcome was locked in when it closed. Hero stats give the
   operator a track record at a glance: total realized P/L, trades closed,
   win rate. Table below lists every closed trade with sort options.

   HistoryView         — top-level layout
   HistoryHero         — three summary cards
   HistoryTable        — closed-trades list, sortable
   ============================================================================*/

function HistoryView() {
  const all = window.POSITIONS || [];
  const closed = all.filter(p => p.status === "closed");
  return (
    <>
      <HistoryHero closed={closed} />
      <div className="section-head">
        <div>
          <div className="section-title">Closed trades</div>
          <div className="section-sub">
            {closed.length === 0
              ? "no closed trades yet — close a position from the Positions tab to see it here"
              : `${closed.length} closed · realized P/L locked in at exit`}
          </div>
        </div>
      </div>
      {closed.length > 0 && <HistoryTable closed={closed} />}
    </>
  );
}

function HistoryHero({ closed }) {
  const realizedSum = closed.reduce((s, p) => s + (p.pnlTotal || 0), 0);
  const wins = closed.filter(p => (p.pnlTotal || 0) > 0).length;
  const losses = closed.filter(p => (p.pnlTotal || 0) < 0).length;
  const winRate = closed.length > 0 ? wins / closed.length : null;
  const avgWin = wins > 0
    ? closed.filter(p => (p.pnlTotal || 0) > 0).reduce((s, p) => s + p.pnlTotal, 0) / wins
    : null;
  const avgLoss = losses > 0
    ? closed.filter(p => (p.pnlTotal || 0) < 0).reduce((s, p) => s + p.pnlTotal, 0) / losses
    : null;
  return (
    <div className="hero positions-hero">
      <div className="card">
        <div className="card-label"><span className="card-label-dot" /> Realized P/L</div>
        <div className="summary-stat">
          <div className={`summary-num tnum ${realizedSum >= 0 ? "positive" : "negative"}`}>
            {realizedSum >= 0 ? "+" : "−"}${Math.abs(realizedSum).toFixed(0)}
          </div>
          <div className="summary-unit">lifetime</div>
        </div>
        <div className="summary-sub mono">
          {closed.length} trade{closed.length === 1 ? "" : "s"} closed
        </div>
      </div>
      <div className="card">
        <div className="card-label"><span className="card-label-dot" /> Win rate</div>
        <div className="summary-stat">
          <div className="summary-num tnum">
            {winRate === null ? "—" : `${(winRate * 100).toFixed(0)}%`}
          </div>
          <div className="summary-unit">
            {winRate === null ? "no closed trades" : `${wins}W / ${losses}L`}
          </div>
        </div>
      </div>
      <div className="card">
        <div className="card-label"><span className="card-label-dot" /> Avg outcome</div>
        <div className="diff-row">
          <div>
            <div className="diff-label">Avg win</div>
            <div className="diff-num added tnum">
              {avgWin === null ? "—" : `+$${Math.abs(avgWin).toFixed(0)}`}
            </div>
          </div>
        </div>
        <div className="diff-row">
          <div>
            <div className="diff-label">Avg loss</div>
            <div className="diff-num removed tnum">
              {avgLoss === null ? "—" : `−$${Math.abs(avgLoss).toFixed(0)}`}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

const HISTORY_SORTS = {
  date_closed_desc: { label: "Closed (newest)", cmp: (a, b) => (b.dateClosed || "").localeCompare(a.dateClosed || "") },
  date_closed_asc:  { label: "Closed (oldest)", cmp: (a, b) => (a.dateClosed || "").localeCompare(b.dateClosed || "") },
  pnl_desc:         { label: "P/L (best)",     cmp: (a, b) => (b.pnlTotal || 0) - (a.pnlTotal || 0) },
  pnl_asc:          { label: "P/L (worst)",    cmp: (a, b) => (a.pnlTotal || 0) - (b.pnlTotal || 0) },
  return_desc:      { label: "Return % (best)", cmp: (a, b) => _ret(b) - _ret(a) },
  return_asc:       { label: "Return % (worst)", cmp: (a, b) => _ret(a) - _ret(b) },
  ticker:           { label: "Ticker (A→Z)",    cmp: (a, b) => a.ticker.localeCompare(b.ticker) },
  days_held_desc:   { label: "Days held (longest)", cmp: (a, b) => (b.daysHeld || 0) - (a.daysHeld || 0) },
};

function _ret(p) {
  return p.pnlPerShare !== null && p.entryPrice
    ? (p.pnlPerShare / Math.abs(p.entryPrice))
    : 0;
}

function HistoryTable({ closed }) {
  const [sortKey, setSortKey] = useState("date_closed_desc");
  const sorted = useMemo(() => {
    const list = [...closed];
    list.sort(HISTORY_SORTS[sortKey].cmp);
    return list;
  }, [closed, sortKey]);
  return (
    <>
      <div className="history-toolbar">
        <select className="sort-select" value={sortKey} onChange={(e) => setSortKey(e.target.value)}>
          {Object.entries(HISTORY_SORTS).map(([k, v]) => (
            <option key={k} value={k}>Sort: {v.label}</option>
          ))}
        </select>
      </div>
      <div className="ideas-card">
        <table className="ideas-table positions-table">
          <thead>
            <tr>
              <th>Ticker</th>
              <th>Strategy</th>
              <th>Strikes</th>
              <th>Opened</th>
              <th>Closed</th>
              <th>Days</th>
              <th>Qty</th>
              <th>Cost</th>
              <th>Exit</th>
              <th>Realized $</th>
              <th>Return %</th>
            </tr>
          </thead>
          <tbody>
            {sorted.map(p => <HistoryRow key={p.id} p={p} />)}
          </tbody>
        </table>
      </div>
    </>
  );
}

function HistoryRow({ p }) {
  const isCredit = p.isCredit;
  const costDollars = Math.abs(p.entryPrice) * 100 * p.quantity;
  const costDisplay = `$${costDollars.toFixed(0)} ${isCredit ? "cr" : "db"}`;
  const exitDisplay = p.exitPrice !== null && p.exitPrice !== undefined
    ? `$${Math.abs(p.exitPrice).toFixed(2)}`
    : "—";
  const pct = _ret(p) * 100;
  return (
    <tr>
      <td>
        <a
          className="ticker-symbol mono ticker-link"
          href={`https://www.tradingview.com/chart/?symbol=${p.ticker}`}
          target="_blank"
          rel="noopener noreferrer"
          title={`Open ${p.ticker} on TradingView Supercharts`}
        >
          {p.ticker}
        </a>
      </td>
      <td><span className={`strat-pill ${p.strategy}`}>{p.strategy}</span></td>
      <td className="mono tnum" style={{ color: "var(--text-2)" }}>
        {p.shortK !== null && p.shortK !== undefined ? `$${p.shortK.toFixed(2)} / ` : ""}
        {p.longK !== null && p.longK !== undefined ? `$${p.longK.toFixed(2)}` : "—"}
      </td>
      <td className="mono tnum" style={{ color: "var(--text-2)" }}>{p.dateOpened}</td>
      <td className="mono tnum" style={{ color: "var(--text-2)" }}>{p.dateClosed}</td>
      <td className="mono tnum" style={{ color: "var(--text-2)" }}>{p.daysHeld !== null ? `${p.daysHeld}d` : "—"}</td>
      <td className="mono tnum">{p.quantity}</td>
      <td className={`mono tnum cost-cell ${isCredit ? "credit" : "debit"}`}>{costDisplay}</td>
      <td className="mono tnum">{exitDisplay}</td>
      <td className={`mono tnum ${p.pnlTotal !== null && p.pnlTotal >= 0 ? "positive" : "negative"}`}>
        {p.pnlTotal === null ? "—" : `${p.pnlTotal >= 0 ? "+" : "−"}$${Math.abs(p.pnlTotal).toFixed(0)}`}
      </td>
      <td className={`mono tnum ${pct >= 0 ? "positive" : "negative"}`}>
        {pct >= 0 ? "+" : ""}{pct.toFixed(1)}%
      </td>
    </tr>
  );
}

/* ----- Help Modal ---------------------------------------------------------- */
// Centralized definitions panel — replaces the per-column glossary popovers
// and the strategy-pill hover. One ? button in the top bar opens this; the
// goal is "one place to learn the vocabulary" rather than scattered hovers
// the operator has to discover individually.
function HelpModal({ open, onClose }) {
  useEffect(() => {
    if (!open) return;
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [open, onClose]);

  if (!open) return null;
  return (
    <>
      <div className="drawer-backdrop open" onClick={onClose} style={{ zIndex: 50 }} />
      <div className="help-modal" role="dialog" aria-label="Definitions and calculations">
        <div className="help-modal-head">
          <div>
            <div className="help-modal-title">Definitions &amp; calculations</div>
            <div className="help-modal-sub">how each column and metric is computed</div>
          </div>
          <button className="drawer-close" onClick={onClose} aria-label="Close">
            <Icon.Close />
          </button>
        </div>
        <div className="help-modal-body">
          <section className="help-section">
            <div className="help-section-title">Strategies</div>
            {Object.entries(window.STRATEGY_META).map(([code, meta]) => (
              <div key={code} className="help-row">
                <span className={`strat-pill ${code}`} style={{ minWidth: 40, textAlign: "center" }}>{code}</span>
                <div>
                  <div className="help-row-title">{meta.name}</div>
                  <div className="help-row-body">{meta.thesis}</div>
                </div>
              </div>
            ))}
          </section>
          <section className="help-section">
            <div className="help-section-title">Columns</div>
            <div className="help-row">
              <div className="help-row-key">Credit / Debit</div>
              <div className="help-row-body">
                <strong>Per contract</strong> (= per-share × 100). PCS/CSP collect a credit (CR — you receive cash), CDS/LC pay a debit (DB — you pay cash). One contract controls 100 shares, so what you see is what hits your account at fill.
              </div>
            </div>
            <div className="help-row">
              <div className="help-row-key">Max Profit</div>
              <div className="help-row-body">
                <strong>Per contract</strong> theoretical maximum at expiration. Spreads (CDS, PCS) cap at width − debit (CDS) or = credit (PCS). Long calls are unbounded; CSP max = the credit collected.
              </div>
            </div>
            <div className="help-row">
              <div className="help-row-key">POP</div>
              <div className="help-row-body">
                <strong>Probability of Profit.</strong> Approximated from option deltas — for credit strategies it's the chance the short leg finishes OTM; for CDS it's a breakeven-aware interpolation between the two deltas. Rough heuristic; fine for ranking, not for risk-of-ruin math.
              </div>
            </div>
            <div className="help-row">
              <div className="help-row-key">IV Rank</div>
              <div className="help-row-body">
                <strong>Implied Volatility Rank</strong> from Tastytrade MarketMetrics — where current IV sits in its trailing 252-day range. <em>1.0</em> = at the high (premium expensive); <em>0.0</em> = at the low (premium cheap). PCS/CSP want HIGH IVR (rich premium to sell); CDS/LC want LOW IVR (cheap premium to buy).
              </div>
            </div>
            <div className="help-row">
              <div className="help-row-key">Confluence</div>
              <div className="help-row-body">
                Weighted blend of 5 components, with <strong>strategy-specific weights</strong>:
                <br />
                <br />
                <strong>Debit (LC, CDS) — direction-driven:</strong> Technical match 35% · IV alignment 20% · Spread tightness 20% · Liquidity 15% · DTE sweet spot 10%.
                <br />
                <strong>Credit (CSP, PCS) — premium-driven:</strong> IV alignment 35% · Technical match 25% · Spread tightness 15% · Liquidity 15% · DTE sweet spot 10%.
                <br />
                <br />
                Components: <em>IV alignment</em> (does IV regime match the strategy — high IV good for sellers, low IV good for buyers), <em>technical match</em> (Finviz bullish score), <em>liquidity</em> (primary-leg OI / 2000), <em>spread tightness</em> (1 − (ask − bid) / mid), <em>DTE sweet spot</em> (peaks at 35 DTE).
                <br />
                <br />
                <strong>Validated</strong> against historical Polygon options chains (Phase 4h backtest): LC top quintile averaged +$377/contract higher P/L than bottom quintile; CDS +$181. Credit-strategy results were regime-confounded (bull market). Treat the score as "expected-value rank for direction-driven trades, premium-quality rank for credit trades" — not as P/L prediction.
              </div>
            </div>
            <div className="help-row">
              <div className="help-row-key">Trend</div>
              <div className="help-row-body">
                12-day underlying-price sparkline derived from <code>option_snapshots.underlying_price</code>. Renders as <em>—</em> until ≥2 distinct trading days of scan history exist for the ticker.
              </div>
            </div>
          </section>
          <section className="help-section">
            <div className="help-section-title">Reading the row</div>
            <div className="help-row">
              <div className="help-row-key">Strikes (Short / Long)</div>
              <div className="help-row-body">
                Spreads show <em>short / long</em>. Single-leg trades (LC, CSP) show only the long-side strike. Long Call: the strike you buy. CDS: long is the lower call you buy, short is the higher call you sell. PCS: long is the lower put you buy for protection, short is the higher put you sell for premium.
              </div>
            </div>
            <div className="help-row">
              <div className="help-row-key">Confluence bar</div>
              <div className="help-row-body">
                Visual readout of the score 0–100%. Click any row for the per-component breakdown in the drawer.
              </div>
            </div>
          </section>
          <section className="help-section" style={{ borderTop: "1px solid var(--border)" }}>
            <div className="help-disclaimer">
              Strategy parameters come from the tastytrade methodology and conventional retail-options resources (delta bands, DTE sweet spot, IV-rank gates, OI minimums). Confluence weights are author-picked defaults. <strong>None of this has been backtested in this codebase.</strong>
            </div>
          </section>
        </div>
      </div>
    </>
  );
}

/* ----- Refresh controller --------------------------------------------------
 * Owns the polling lifecycle for the dashboard's two refresh buttons.
 *
 * Flow:
 *   1. User clicks → POST /refresh/{light,universe} → server flips state to running
 *   2. Hook polls /refresh/status every 1s while running
 *   3. When server reports finished_at, we wait one more poll cycle (so
 *      the dashboard's static data.js has a chance to be regenerated by
 *      build_dashboard before we reload), then trigger a full window
 *      reload to pick up fresh window.IDEAS.
 *
 * Errors surface in `state.error` and are shown in the banner — the
 * server keeps the error state until the next refresh starts.
 * --------------------------------------------------------------------------- */
function useRefreshController() {
  const [state, setState] = useState({
    status: "idle", kind: null, stage: null,
    current: 0, total: 0, message: "", error: null,
    startedAt: null, finishedAt: null, elapsedSeconds: null,
  });
  const finishedRef = useRef(false);

  const fetchStatus = async () => {
    try {
      const r = await fetch("/refresh/status", { cache: "no-store" });
      const j = await r.json();
      setState({
        status: j.status, kind: j.kind, stage: j.stage,
        current: j.current, total: j.total,
        message: j.message, error: j.error,
        startedAt: j.started_at, finishedAt: j.finished_at,
        elapsedSeconds: j.elapsed_seconds,
      });
      return j;
    } catch (e) {
      // Likely the user is running plain http.server (no /refresh API)
      // — silently ignore; the buttons will just not trigger.
      return null;
    }
  };

  // Poll while running; auto-reload after refresh completes.
  useEffect(() => {
    if (state.status !== "running") return;
    finishedRef.current = false;
    const id = setInterval(async () => {
      const j = await fetchStatus();
      // First pass: when server reports idle and finishedRef hasn't been
      // marked, mark it so the NEXT poll cycle triggers the reload —
      // gives the dashboard build a moment to write fresh data.js.
      if (j && j.status === "idle" && j.finished_at && !finishedRef.current) {
        finishedRef.current = true;
        if (!j.error) {
          // Brief "complete" state shown to the operator, then reload.
          setTimeout(() => window.location.reload(), 800);
        }
      }
    }, 1000);
    return () => clearInterval(id);
  }, [state.status]);

  const trigger = async (kind) => {
    if (state.status === "running") return;
    setState(s => ({ ...s, status: "running", kind, stage: "starting",
                     message: "Starting...", current: 0, total: 0, error: null }));
    try {
      const r = await fetch(`/refresh/${kind}`, { method: "POST" });
      if (!r.ok) {
        const text = await r.text();
        throw new Error(`POST /refresh/${kind} → ${r.status}: ${text}`);
      }
      // Poll once immediately so the banner picks up the real first-stage state.
      await fetchStatus();
    } catch (e) {
      setState(s => ({ ...s, status: "idle", error: e.message,
                       message: "Refresh failed to start" }));
    }
  };

  // On mount, sync once in case a refresh is already running (e.g. the
  // user opened a second tab while one is mid-flight).
  useEffect(() => { fetchStatus(); }, []);

  return { state, trigger };
}

/* ----- Tab-visible auto-refresh ------------------------------------------
 * Triggers a light (Tier 1) refresh every AUTO_REFRESH_MINUTES when the
 * dashboard tab is in the foreground. Pauses entirely when the tab is
 * hidden so we're not running scans against an audience of zero. Also
 * runs once immediately when the tab becomes visible after being hidden
 * for longer than the cadence — so "I came back to the tab" feels like
 * "data is current," not "let me wait 8 minutes."
 *
 * Off by default; the operator opts in via a toggle. Storing the
 * preference in localStorage so it persists across reloads.
 * --------------------------------------------------------------------------- */
const AUTO_REFRESH_MINUTES = 8;
const AUTO_REFRESH_KEY = "os-auto-refresh";

function useAutoRefresh(refresh) {
  const [enabled, setEnabledState] = useState(() => {
    return localStorage.getItem(AUTO_REFRESH_KEY) === "1";
  });
  const lastRunRef = useRef(0);

  const setEnabled = (val) => {
    setEnabledState(val);
    localStorage.setItem(AUTO_REFRESH_KEY, val ? "1" : "0");
  };

  useEffect(() => {
    if (!enabled) return;

    const tickIfDue = () => {
      // Don't double-fire if a refresh is already in flight.
      if (refresh.state.status === "running") return;
      // Don't refresh if the tab is hidden.
      if (document.hidden) return;
      const now = Date.now();
      const since = now - lastRunRef.current;
      if (since < AUTO_REFRESH_MINUTES * 60 * 1000) return;
      lastRunRef.current = now;
      refresh.trigger("light");
    };

    // Tick once immediately if it's been long enough since the last run
    // — covers "came back to the tab after lunch" without a 8-min wait.
    tickIfDue();

    const interval = setInterval(tickIfDue, 30 * 1000); // check every 30s
    const onVis = () => { if (!document.hidden) tickIfDue(); };
    document.addEventListener("visibilitychange", onVis);

    return () => {
      clearInterval(interval);
      document.removeEventListener("visibilitychange", onVis);
    };
  }, [enabled, refresh]);

  return { enabled, setEnabled };
}

/* ----- Refresh banner — sticky progress strip during a running refresh ---- */
function RefreshBanner({ state }) {
  if (state.status !== "running" && !state.error) return null;
  const pct = state.total > 0 ? Math.min(100, Math.round((state.current / state.total) * 100)) : 0;
  const indeterminate = state.total === 0;
  const label = state.kind === "universe" ? "Universe refresh" :
                state.kind === "light" ? "Quick refresh" : "Refreshing";
  return (
    <div className={`refresh-banner ${state.error ? "is-error" : ""}`}>
      <div className="refresh-banner-inner">
        <div className="refresh-banner-text">
          <strong>{label}:</strong> {state.message || "..."}
          {state.elapsedSeconds !== null && state.elapsedSeconds !== undefined && (
            <span className="refresh-banner-elapsed mono"> · {state.elapsedSeconds.toFixed(0)}s</span>
          )}
        </div>
        <div className={`refresh-banner-bar ${indeterminate ? "indeterminate" : ""}`}>
          <div className="refresh-banner-fill" style={indeterminate ? {} : { width: `${pct}%` }} />
        </div>
      </div>
    </div>
  );
}

/* ----- Topbar -------------------------------------------------------------- */
function TopBar({ theme, onToggleTheme, refresh, tab, onTabChange }) {
  const [helpOpen, setHelpOpen] = useState(false);
  const auto = useAutoRefresh(refresh);
  const running = refresh.state.status === "running";
  return (
    <>
      <header className="topbar">
        <div className="topbar-inner">
          <div className="brand">
            <div className="brand-mark"><Icon.Logo /></div>
            Confluence
            <span className="brand-meta">/ options scanner</span>
          </div>

          {/* Top-level tab toggle: Ideas (the scanner) vs Positions
              (manual portfolio tracker). State lives in App via the
              ``tab`` prop; ``onTabChange`` updates it + persists to
              localStorage so the user lands back where they left. */}
          <div className="tab-toggle" role="tablist">
            <button
              role="tab"
              aria-selected={tab === "ideas"}
              className={tab === "ideas" ? "active" : ""}
              onClick={() => onTabChange("ideas")}
            >
              Ideas
            </button>
            <button
              role="tab"
              aria-selected={tab === "positions"}
              className={tab === "positions" ? "active" : ""}
              onClick={() => onTabChange("positions")}
            >
              Positions
              {(window.POSITIONS || []).filter(p => p.status === "open").length > 0 && (
                <span className="tab-count">
                  {(window.POSITIONS || []).filter(p => p.status === "open").length}
                </span>
              )}
            </button>
            <button
              role="tab"
              aria-selected={tab === "history"}
              className={tab === "history" ? "active" : ""}
              onClick={() => onTabChange("history")}
            >
              History
            </button>
          </div>

          <div className="scan-pill">
            <span className="live-dot" />
            <span>scan</span>
            <span className="mono" style={{ color: "var(--text)" }}>
              {window.SCAN.id.slice(0, 8)}
            </span>
            <span style={{ color: "var(--text-3)" }}>·</span>
            <span>{formatLocalScanTime(window.SCAN.scannedAtIso, window.SCAN.scannedAt)}</span>
          </div>

          {/* Tier 1 refresh: re-extract chains for current top-30. ~60-90s. */}
          <button
            className="icon-btn refresh-btn"
            onClick={() => refresh.trigger("light")}
            disabled={running}
            title="Quick refresh — fresh prices/IVs on current top-30 (~60-90s)"
            aria-label="Quick refresh"
          >
            <Icon.Refresh />
          </button>
          {/* Tier 2 refresh: Finviz universe re-rank + scan. ~75-135s. */}
          <button
            className="icon-btn refresh-btn"
            onClick={() => refresh.trigger("universe")}
            disabled={running}
            title="Universe refresh — re-pull Finviz, re-rank top-30, scan (~75-135s)"
            aria-label="Universe refresh"
          >
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
              <circle cx="12" cy="12" r="9" />
              <path d="M3 12h18" />
              <path d="M12 3a14 14 0 0 1 0 18a14 14 0 0 1 0-18z" />
            </svg>
          </button>

          {/* Auto-refresh toggle — re-runs the quick refresh every 8 min
              while the dashboard tab is visible. Indigo dot when on. */}
          <button
            className={`icon-btn auto-refresh-btn ${auto.enabled ? "is-on" : ""}`}
            onClick={() => auto.setEnabled(!auto.enabled)}
            title={auto.enabled
              ? `Auto-refresh: ON (every ${AUTO_REFRESH_MINUTES} min while tab is visible) — click to disable`
              : `Auto-refresh: OFF — click to enable (every ${AUTO_REFRESH_MINUTES} min while tab is visible)`}
            aria-label={auto.enabled ? "Disable auto-refresh" : "Enable auto-refresh"}
          >
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
              <circle cx="12" cy="12" r="9" />
              <polyline points="12 7 12 12 15 14" />
            </svg>
            {auto.enabled && <span className="auto-refresh-dot" />}
          </button>

          <button className="icon-btn" onClick={() => setHelpOpen(true)} title="Definitions & calculations" aria-label="Help">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
              <circle cx="12" cy="12" r="9" />
              <path d="M9.5 9a2.5 2.5 0 1 1 4 2c-.6.5-1.5 1-1.5 2" />
              <line x1="12" y1="17" x2="12" y2="17.01" />
            </svg>
          </button>
          <button className="icon-btn" onClick={onToggleTheme} title={theme === "dark" ? "Light mode" : "Dark mode"}>
            {theme === "dark" ? <Icon.Sun /> : <Icon.Moon />}
          </button>
        </div>
      </header>
      <RefreshBanner state={refresh.state} />
      <HelpModal open={helpOpen} onClose={() => setHelpOpen(false)} />
    </>
  );
}

Object.assign(window, {
  Icon, Sparkline, StratPill, ConfluenceSpectrum, Hero,
  Toolbar, IdeaRow, IdeasTable, GroupedView,
  ConfRing, PayoffDiagram, UnderlyingChart, Drawer, TopBar,
  RefreshBanner, useRefreshController,
  PositionsView, AddPositionModal, ClosePositionModal, HistoryView,
  PositionDrawer,
  fmtMoney, fmtPct,
});
