/* positions.jsx — Open Positions tab.
   Left: sortable table of active positions with expandable parser detail.
   Right: Latest Position Events rail (live ticking).
*/

function ActionCell({ action, confidence }) {
  return (
    <div className="flex gap-12" style={{ alignItems: "center" }}>
      <ActionPill kind={action} />
    </div>
  );
}

function ParserDetail({ event }) {
  if (!event) return null;
  return (
    <div className="parser-detail">
      <div>
        <h4>Raw Discord message</h4>
        <div className="raw-msg">{event.raw}</div>
        <div className="meta-row">
          <span>channel <strong>{event.channel}</strong></span>
          <span>msg <strong className="mono">{event.messageId}</strong></span>
          <span>scraped <strong>{fmtTime(event.ts)}</strong></span>
        </div>
        <h4 style={{ marginTop: 18 }}>Why Claude tagged this</h4>
        <div className="reasoning">
          {event.reasoning.summary.split(/("[^"]+")/).map((part, i) =>
            part.startsWith('"') ? <span key={i} className="highlight">{part}</span> : <span key={i}>{part}</span>
          )}
        </div>
      </div>
      <div>
        <h4>Extracted event JSON</h4>
        <pre>{
`{
  `}<span className="k">"ticker"</span>{`:     `}<span className="s">"{event.ticker}"</span>{`,
  `}<span className="k">"action"</span>{`:     `}<span className="s">"{event.action}"</span>{`,
  `}<span className="k">"direction"</span>{`:  `}<span className="s">"{event.direction}"</span>{`,
  `}<span className="k">"contract"</span>{`:   `}<span className="s">{event.contract ? `"${event.contract}"` : "null"}</span>{`,
  `}<span className="k">"price"</span>{`:      `}<span className="n">{event.price == null ? "null" : event.price.toFixed(2)}</span>{`,
  `}<span className="k">"stop_loss"</span>{`:  `}<span className="n">{event.stopLoss == null ? "null" : event.stopLoss}</span>{`,
  `}<span className="k">"confidence"</span>{`: `}<span className="s">"{event.confidence}"</span>{`,
  `}<span className="k">"raw_note"</span>{`:   `}<span className="s">"{event.raw.length > 60 ? event.raw.slice(0, 57) + "…" : event.raw}"</span>{`,
  `}<span className="k">"matched"</span>{`:    [`}{event.reasoning.matched.map((m, i) => [
            i ? <span key={"c"+i}>, </span> : null,
            <span key={"v"+i} className="s">"{m}"</span>
          ])}{`]
}`}</pre>
        <div className="meta-row">
          <span>model <strong>claude-haiku-4-5</strong></span>
          <span>latency <strong>{(280 + (event.id.charCodeAt(4)*7 % 220))}ms</strong></span>
          <span>tokens <strong>{120 + (event.id.charCodeAt(5) % 80)}</strong></span>
        </div>
      </div>
    </div>
  );
}

// Every interaction with a trade (open / add / trim / close…), in order.

// Position-level identity edit — corrects ticker / contract / strike / expiry /
// type across EVERY event in the position in one write. Identity fields only:
// price and action are per-event truths and stay on the single-event override.
function PositionIdentityEdit({ position, onClose, onSaved }) {
  const p = position;
  const [ticker, setTicker] = useState(p.ticker || "");
  const [kind, setKind] = useState(p.instrument === "shares" ? "shares" : "option");
  const [optType, setOptType] = useState(p.optionType || "call");
  const [strike, setStrike] = useState(p.strike != null ? String(p.strike) : "");
  const [expiry, setExpiry] = useState(p.expiry || "");
  const [busy, setBusy] = useState(false);
  const ids = (p.events || []).map(e => e.id).filter(Boolean);

  async function save() {
    const isOpt = kind === "option";
    const body = {
      event_ids: ids,
      ticker: ticker.trim(),
      instrument: kind,
      contract: isOpt ? (buildContractText(strike, expiry, optType) || null) : null,
      strike: isOpt && strike.trim() !== "" ? Number(strike) : null,
      expiry: isOpt ? (expiry.trim() || null) : null,
      option_type: isOpt ? optType : null,
    };
    setBusy(true);
    try {
      const r = await tapeSend(apiBase() + "/api/positions/identity", {
        method: "PATCH", headers: { "content-type": "application/json" },
        body: JSON.stringify(body),
      });
      const j = await r.json().catch(() => ({}));
      toast.success(`Updated ${j.updated ?? ids.length} event${(j.updated ?? ids.length) === 1 ? "" : "s"}`);
      if (onSaved) onSaved(j.events || []);
      onClose();
    } catch (e) { toast.error("Couldn't update - " + e.message); }
    setBusy(false);
  }

  return (
    <div className="modal-back" onMouseDown={() => !busy && onClose()}>
    <div className="modal manage-modal" role="dialog" aria-modal="true" onMouseDown={(ev) => ev.stopPropagation()}>
      <div className="calc-hdr">
        <div>
          <div className="calc-title">{I("edit", { size: 16 })} Edit position</div>
          <div className="calc-sub mono">{p.analyst} · ${p.ticker} · applies to ALL {ids.length} event{ids.length === 1 ? "" : "s"}</div>
        </div>
        <button className="icon-btn" title="Close" onClick={onClose}>{I("close", { size: 16 })}</button>
      </div>
      <div className="manage-body">
        <p className="sub" style={{ marginTop: 0 }}>
          Use this when the parser read the contract differently across messages. Editing those events
          one at a time <strong>splits</strong> the position instead of fixing it — this rewrites every
          event together. Prices and actions are untouched; correct those individually.
        </p>
        <div className="manage-fields">
          <label className="calc-field">
            <span>Ticker</span>
            <div className="calc-input"><span className="calc-pre">$</span><input value={ticker} onChange={ev => setTicker(ev.target.value)} placeholder="NVDA" /></div>
          </label>
          <label className="calc-field">
            <span>Instrument</span>
            <select className="select" value={kind} onChange={ev => setKind(ev.target.value)}>
              <option value="option">option</option><option value="shares">shares</option>
            </select>
          </label>
          {kind === "option" && <>
            <label className="calc-field">
              <span>Strike</span>
              <div className="calc-input"><input value={strike} onChange={ev => setStrike(ev.target.value)} inputMode="decimal" placeholder="210" /></div>
            </label>
            <label className="calc-field">
              <span>Expiry</span>
              <div className="calc-input"><input value={expiry} onChange={ev => setExpiry(ev.target.value)} placeholder="6/22" /></div>
            </label>
            <label className="calc-field">
              <span>Type</span>
              <select className="select" value={optType} onChange={ev => setOptType(ev.target.value)}>
                <option value="call">call</option><option value="put">put</option>
              </select>
            </label>
          </>}
        </div>
        <div className="manage-acts">
          <button className="btn" onClick={onClose} disabled={busy}>Cancel</button>
          <button className="btn primary" onClick={save} disabled={busy || !ticker.trim()}>
            {busy ? "Saving..." : `Apply to ${ids.length} event${ids.length === 1 ? "" : "s"}`}
          </button>
        </div>
      </div>
    </div>
    </div>
  );
}

// "6/22 210C" from the parts — the same shape the bot's buildContract writes, so
// a corrected position keys identically to a freshly-parsed one.
function buildContractText(strike, expiry, optType) {
  const s = String(strike || "").trim(), e = String(expiry || "").trim();
  if (!s && !e) return null;
  const t = optType === "put" ? "P" : "C";
  return [e, s ? s + t : null].filter(Boolean).join(" ");
}

function TradeTimeline({ events, onEdit, position, onEditPosition }) {
  const ordered = [...events].sort((a, b) => a.ts - b.ts);
  // A position whose events disagree about strike / expiry / contract is really
  // TWO positions to the aggregator — the history splits and one half loses its
  // entry price. Fixing that one event at a time cannot work (each save re-splits
  // it differently), so the cascade edits every event in the position at once.
  const inconsistent = position && (
    new Set(ordered.map(e => (e.strike == null ? "" : String(e.strike)))).size > 1 ||
    new Set(ordered.map(e => e.optionType || "")).size > 1);
  return (
    <div className="trade-timeline">
      {onEditPosition && position && (
        <div className="tl-poshead">
          {inconsistent && (
            <span className="tl-warn" title="These events disagree about the contract, so this shows as more than one position">
              contract mismatch across events
            </span>
          )}
          <button type="button" className="btn tl-posedit"
            title="Correct ticker / contract / strike / expiry across EVERY event in this position"
            onClick={(ev) => { ev.stopPropagation(); onEditPosition(position); }}>
            {I("edit", { size: 12 })} Edit position
          </button>
        </div>
      )}
      {ordered.map((e, i) => {
        // Staircase the lifecycle so the entry anchors at the left and the
        // follow-ups (add/trim/hold/cut/close) step in beneath it — easier to
        // read at a glance. Capped at 2 steps so a long timeline never marches
        // off the right edge.
        const level = Math.min(i, 2);
        // Exits (trim/cut/close) are always a fill ("@"); only an open holding
        // carries a mark ("-> n"). Tolerate older data that mis-filed an "@".
        const exitAction = ["close", "cut", "trim"].includes(e.action);
        const fill = e.price != null ? e.price : (exitAction ? e.currentPrice : null);
        const mark = (!exitAction && e.currentPrice != null) ? e.currentPrice : null;
        return (
          <div key={e.id || i} className={cx("tl-row", i === 0 && "tl-entry")} style={{ "--tl-level": level }}>
            <span className="tl-mark" aria-hidden="true" />
            <span className="tl-time mono">{new Date(e.ts).toLocaleString("en-US", { month: "numeric", day: "numeric", hour: "numeric", minute: "2-digit" })}</span>
            <ActionPill kind={e.action} />
            <span className="tl-detail mono">
              {isOption(e) && contractText(e) ? <span className="contract">{contractText(e)}</span> : <span style={{ color: "var(--fg-3)" }}>shares</span>}
              {fill != null && <span> @ {fmtNum(fill)}</span>}
              {mark != null && <span style={{ color: "var(--fg-2)" }}> → {fmtNum(mark)}</span>}
              {e.avgPrice != null && <span style={{ color: "var(--c-watch)" }}> · avg {fmtNum(e.avgPrice)}</span>}
              {e.stopLoss != null && <span style={{ color: "var(--c-down)" }}> · SL {fmtNum(e.stopLoss)}</span>}
              {e.source === "portal" && <span className="manual-tag" title={e.createdBy ? "Logged via portal by " + e.createdBy : "Logged via portal"}>portal</span>}
            </span>
            <span className="tl-note">{e.raw}</span>
            {onEdit && <button type="button" className="tl-edit" title="Override / correct this event" onClick={(ev) => { ev.stopPropagation(); onEdit(e); }}>{I("edit", { size: 13 })}</button>}
          </div>
        );
      })}
    </div>
  );
}

// Mobile Open Positions — a compact accordion. Each row shows only ticker ·
// instrument · analyst · entry→now; tap a row to reveal the full metrics, last
// alert, and the interaction timeline. (Desktop keeps the table.)
function MobilePositionList({ rows, analysts, onOpenChart, onCalc, onManage, canManageRow, showParser, onEditEvent, canEditEvent, onEditPosition }) {
  const [open, setOpen] = useState(null);
  if (!rows.length) return <Empty msg="No positions match this filter." />;
  return (
    <div className="mpos-list">
      {rows.map(p => {
        const isOpen = open === p.key;
        const opt = isOption(p);
        const basis = p.avgPrice != null ? p.avgPrice : p.entryPrice;
        const pnl = (basis && p.markPrice != null) ? (p.markPrice - basis) / basis : null;
        const up = pnl != null && pnl >= 0;
        const lastEvent = p.events[p.events.length - 1];
        const rcy = recencyBucket(p.updatedTs);
        return (
          <div key={p.key} className={cx("mpos", isOpen && "open", pnl != null && (up ? "pnl-up" : "pnl-down"))}>
            <button type="button" className="mpos-head" onClick={() => setOpen(isOpen ? null : p.key)}>
              {/* Top: identity on the left, status + chevron on the right. */}
              <div className="mpos-top">
                <span className="mpos-id">
                  <span className="ticker">${p.ticker}</span>
                  <InstrumentTag event={p} />
                  {window.LEDGER_UI && opt && dteOf(p.expiry) != null && (
                    <span className={cx("dte-chip", dteTier(dteOf(p.expiry)))}>{dteLabel(dteOf(p.expiry))}</span>
                  )}
                </span>
                <span className="mpos-top-r">
                  {!isOpen && rcy === "live" && <span className="rcy-new">NEW</span>}
                  <span className="mpos-chev">{I(isOpen ? "chevDown" : "chevRight", { size: 16 })}</span>
                </span>
              </div>
              {/* Contract on its own line so nothing above it crowds. */}
              {opt && (p.strike != null || p.expiry) && (
                <div className="mpos-cx mono">
                  {p.strike != null && <span className="ic-strike">{strikeText(p)}</span>}
                  {p.strike != null && p.expiry && <span className="ic-sep"> · </span>}
                  {p.expiry && <span>{fmtExpiry(p.expiry)}</span>}
                </div>
              )}
              {/* Body: analyst on the left; a right-aligned numbers stack that a long
                  analyst name can never push around (leads with the % — the answer). */}
              <div className="mpos-bd">
                <AnalystChip analyst={p.analyst} analysts={analysts} />
                <span className="mpos-nums">
                  {basis ? (p.markPrice != null ? (
                    <>
                      {pnl != null && <span className={cx("mpos-pct", up ? "up" : "down")}>{fmtPct(pnl)}</span>}
                      <span className="mpos-flow mono">
                        {fmtPrice(basis, p.currency)} <span className="mpos-arrow">→</span> <LiveNum q={p._lq}>{fmtPrice(p.markPrice, p.currency)}</LiveNum>
                        {p._lq && <LiveChip meta={liveQuoteMeta()} />}
                      </span>
                      {(( !p._lq && p._lqUnderlying) || (window.LEDGER_UI && !p._lq)) && (
                        <span className="mpos-metaline mono">
                          {!p._lq && p._lqUnderlying && (
                            <span className="lv-underlying" data-tour="live-underlying">{p.ticker} <LiveNum q={p._lqUnderlying}>{fmtNum(p._lqUnderlying.price)}</LiveNum><span className={cx("lv-u-arrow", p._lqUnderlying.dir >= 0 ? "up" : "down")}>{p._lqUnderlying.dir >= 0 ? "▲" : "▼"}</span></span>
                          )}
                          {!p._lq && p._lqUnderlying && window.LEDGER_UI && <span className="mpos-dot"> · </span>}
                          {window.LEDGER_UI && !p._lq && <span className={cx("mark-age", markAge(p.updatedTs))}>{fmtAgo(p.updatedTs)}</span>}
                        </span>
                      )}
                      {p._lq && <span className="mpos-spark" data-tour="live-chart"><LiveSpark hist={liveSparkHist(p.ticker)} up={pnl != null && pnl >= 0} /></span>}
                    </>
                  ) : (
                    <>
                      <span className="mpos-flow mono">{fmtPrice(basis, p.currency)} <span className="mpos-entry-lbl">entry</span></span>
                      <span className="mpos-metaline mono"><span className="mark-age none">no mark yet</span></span>
                    </>
                  )) : <span className="muted">—</span>}
                </span>
              </div>
            </button>
            {isOpen && (
              <div className="mpos-body">
                <dl className="mpos-grid">
                  <div><dt>Action</dt><dd><ActionPill kind={p.lastAction} /></dd></div>
                  <div><dt>Conf</dt><dd><ConfPill level={p.lastConfidence} /></dd></div>
                  {opt && <div><dt>Expiry</dt><dd>{p.expiry ? fmtExpiry(p.expiry) : "—"}</dd></div>}
                  {opt && <div><dt>Strike</dt><dd>{p.strike != null ? p.strike : "—"}</dd></div>}
                  <div><dt>Stop</dt><dd>{p.stopLoss != null ? <span className="mpos-dn">{fmtNum(p.stopLoss)}</span> : "—"}</dd></div>
                  <div><dt>{window.LEDGER_UI ? "Last analyst update" : "Updated"}</dt><dd>{fmtTime(p.updatedTs, { short: true })}</dd></div>
                </dl>
                {p.lastNote && (
                  <div className="mpos-sec">
                    <span className="mpos-lbl">Last alert</span>
                    <p className="mpos-note">{p.lastNote}</p>
                  </div>
                )}
                <div className="mpos-sec">
                  <span className="mpos-lbl">All interactions · {p.events.length}</span>
                  <TradeTimeline events={p.events} onEdit={canEditEvent && canEditEvent(p) ? onEditEvent : undefined} position={p} onEditPosition={canEditEvent && canEditEvent(p) ? onEditPosition : undefined} />
                </div>
                {showParser && <ParserDetail event={lastEvent} />}
                {(onCalc || onOpenChart || onManage) && (
                  <div className="mpos-btns">
                    {onManage && (!canManageRow || canManageRow(p)) && <button className="btn primary" onClick={() => onManage(p)}>{I("edit", { size: 15 })} Manage</button>}
                    {onCalc && <button className="btn" onClick={() => onCalc(p)}>{I("calc", { size: 15 })} Calculator</button>}
                    {onOpenChart && <button className="btn" onClick={() => onOpenChart(p.ticker)}>{I("chart", { size: 15 })} Chart</button>}
                  </div>
                )}
              </div>
            )}
          </div>
        );
      })}
    </div>
  );
}

// Current unrealized P&L for an open position: basis (avg → entry) vs latest mark.
function posPnl(p) {
  const basis = p.avgPrice != null ? p.avgPrice : p.entryPrice;
  const mark = p.markPrice;
  const pnl = (basis && mark != null) ? (mark - basis) / basis : null;
  return { basis, mark, pnl };
}

// At-a-glance summary strip — leads with P&L so you read the answer first.
// Derived from ALL open positions (independent of the table's filters).
function PositionsSummary({ positions, recaps }) {
  const s = useMemo(() => {
    const open = positions.filter(p => p.status !== "closed" && p.status !== "expired");
    const marks = open.map(posPnl).filter(x => x.pnl != null);
    const greens = marks.filter(x => x.pnl >= 0).length;
    const reds = marks.filter(x => x.pnl < 0).length;
    const avg = marks.length ? marks.reduce((a, x) => a + x.pnl, 0) / marks.length : null;
    let mover = null;
    for (const p of open) { const { pnl } = posPnl(p); if (pnl == null) continue; if (!mover || Math.abs(pnl) > Math.abs(mover.pnl)) mover = { ticker: p.ticker, analyst: p.analyst, pnl }; }
    const weekAgo = Date.now() - 7 * 86400000;
    const closedWk = (recaps || []).filter(r => r.updatedTs >= weekAgo);
    const wWk = closedWk.filter(r => r.pnlPct >= 0).length;
    // Ledger extras: how many open positions expire this week, and how many have
    // gone quiet (no analyst update in 2+ days) — the honesty meter.
    const wkEnd = Date.now() + 7 * 86400000;
    const expiring = open.filter(p => isOption(p) && p.expiry && (() => { const d = dteOf(p.expiry); return d != null && d >= 0 && d <= 7; })()).length;
    const stale = open.filter(p => markAge(p.updatedTs) === "stale").length;
    return { open: open.length, marked: marks.length, greens, reds, avg, mover, closedWk: closedWk.length, wWk, lWk: closedWk.length - wWk, expiring, stale };
  }, [positions, recaps]);

  return (
    <div className="statgrid pos-summary">
      <div className="stat">
        <div className="lbl" title={window.LEDGER_UI ? "Averaged from the prices analysts stated — not market data" : undefined}>
          {I("trend", { size: 11 })} {window.LEDGER_UI ? <>Avg stated P&amp;L</> : <>Open P&amp;L · avg</>}
        </div>
        <div className={cx("val", s.avg != null && (s.avg >= 0 ? "up" : "down"))}>{s.avg != null ? fmtPct(s.avg) : "—"}</div>
        <div className="delta">{s.marked} of {s.open} marked</div>
      </div>
      <div className="stat">
        <div className="lbl">{I("table", { size: 11 })} Open positions</div>
        <div className="val">{s.open}</div>
        <div className="delta">{s.marked ? <><span className="up">{s.greens} ▲</span> · <span className="down">{s.reds} ▼</span></> : "awaiting marks"}</div>
      </div>
      {window.LEDGER_UI ? (
        <div className="stat">
          <div className="lbl">{I("activity", { size: 11 })} Expiring this week</div>
          <div className={cx("val", s.expiring > 0 && "val-warn")}>{s.expiring}</div>
          <div className="delta">{s.stale ? <>{s.stale} not updated in 2d+</> : "all marks current"}</div>
        </div>
      ) : (
        <div className="stat">
          <div className="lbl">{I("activity", { size: 11 })} Biggest mover</div>
          {s.mover
            ? <div className={cx("val", "val-sm", s.mover.pnl >= 0 ? "up" : "down")}><span className="ticker">${s.mover.ticker}</span> {fmtPct(s.mover.pnl)}</div>
            : <div className="val">—</div>}
          <div className="delta">{s.mover ? s.mover.analyst : "no marks yet"}</div>
        </div>
      )}
      <div className="stat">
        <div className="lbl">{I("recap", { size: 11 })} Closed this week</div>
        <div className="val">{s.closedWk}</div>
        <div className="delta">{s.closedWk ? <><span className="up">{s.wWk}W</span> / <span className="down">{s.lWk}L</span></> : "none yet"}</div>
      </div>
    </div>
  );
}

// ── Positions has TWO maintained layouts — flip GLANCE_UI in app.jsx to switch.
//   • Positions (default) — the at-a-glance build: P&L-led table (left bar + big
//     % cell), Winners/Losers/Today segments, details in the expand panel.
//   • PositionsClassic — the prior 12-column table (Expiry/Strike/Stop/Conf
//     inline). Kept so we can revert instantly if the new view doesn't land.
function PositionsClassic({ positions, events, analysts, servers, filter, days, onPickTicker, onOpenChart, onLogEvent, canManageRow, showParser, onEditEvent, canEditEvent, onEditPosition }) {
  const isMobile = useIsMobile();
  const [sort, setSort] = useState({ key: "updatedTs", dir: "desc" });
  const [expanded, setExpanded] = useState(null);
  const [inst, setInst] = useState("all");
  const [anFilter, setAnFilter] = useState(() => new Set());
  const [calcPos, setCalcPos] = useState(null);
  const [managePos, setManagePos] = useState(null);
  const [logOpen, setLogOpen] = useState(false);

  const cutoff = Date.now() - days * 86400000;

  // Analysts that actually have an open position in range — populates the filter.
  const availableAnalysts = useMemo(() => {
    const set = new Set();
    for (const p of positions) {
      if (p.status === "closed" || p.status === "expired") continue;
      if (p.updatedTs < cutoff) continue;
      if (!matchesInstrument(p, inst)) continue;
      set.add(p.analyst);
    }
    return [...set].sort();
  }, [positions, cutoff, inst]);

  const filteredPositions = useMemo(() => {
    const fl = filter.toLowerCase().trim();
    return positions
      .filter(p => p.status !== "closed" && p.status !== "expired")
      .filter(p => p.updatedTs >= cutoff)
      .filter(p => matchesInstrument(p, inst))
      .filter(p => analystMatch(p.analyst, anFilter))
      .filter(p => {
        if (!fl) return true;
        return p.ticker.toLowerCase().includes(fl);
      });
  }, [positions, filter, cutoff, inst, anFilter]);

  const sorted = useMemo(() => {
    const arr = [...filteredPositions];
    const dir = sort.dir === "asc" ? 1 : -1;
    arr.sort((a, b) => {
      const av = a[sort.key], bv = b[sort.key];
      if (typeof av === "number" && typeof bv === "number") return (av - bv) * dir;
      return String(av || "").localeCompare(String(bv || "")) * dir;
    });
    return arr;
  }, [filteredPositions, sort]);

  function setSortKey(k) {
    setSort(s => s.key === k ? { ...s, dir: s.dir === "asc" ? "desc" : "asc" } : { key: k, dir: "desc" });
  }
  const sortInd = (k) => sort.key === k ? (sort.dir === "asc" ? "↑" : "↓") : "↕";

  return (
    <>
    <div className="panel">
      <div className="panel-hdr">
        <div className="panel-title">Open Positions</div>
        <div className="panel-hdr-tools">
          {onLogEvent && <button className="btn primary btn-sm" onClick={() => setLogOpen(true)}>{I("plus", { size: 14 })} Log position</button>}
          <AnalystFilter available={availableAnalysts} selected={anFilter} onChange={setAnFilter} analysts={analysts} />
          <InstrumentFilter value={inst} onChange={setInst} />
          <div className="panel-meta">{sorted.length} active</div>
        </div>
      </div>
      <div className="scroll-y feed-scroll">
        {isMobile ? (
          <MobilePositionList rows={sorted} analysts={analysts} onOpenChart={onOpenChart} onCalc={setCalcPos} onManage={onLogEvent ? setManagePos : undefined} canManageRow={canManageRow} showParser={showParser} onEditEvent={onEditEvent} canEditEvent={canEditEvent} onEditPosition={onEditPosition} />
        ) : (
        <table className="tbl tbl-fixed tbl-cards tbl-wide">
          <colgroup>
            <col style={{ width: "11%" }} />
            <col style={{ width: "6%" }} />
            <col style={{ width: "7%" }} />
            <col style={{ width: "8%" }} />
            <col style={{ width: "6%" }} />
            <col style={{ width: "6%" }} />
            <col style={{ width: "12%" }} />
            <col style={{ width: "9%" }} />
            <col style={{ width: "7%" }} />
            <col style={{ width: "6%" }} />
            <col style={{ width: "10%" }} />
            <col style={{ width: "12%" }} />
          </colgroup>
          <thead>
            <tr>
              <th onClick={() => setSortKey("analyst")} className={cx("sortable", sort.key==="analyst" && "active")}>Analyst <span className="sort-ind">{sortInd("analyst")}</span></th>
              <th onClick={() => setSortKey("ticker")} className={cx("sortable", sort.key==="ticker" && "active")}>Ticker <span className="sort-ind">{sortInd("ticker")}</span></th>
              <th>Instrument</th>
              <th>Expiry</th>
              <th>Strike</th>
              <th>Stop</th>
              <th onClick={() => setSortKey("entryPrice")} className={cx("sortable num", sort.key==="entryPrice" && "active")} style={{textAlign:"right"}}>Entry → Now <span className="sort-ind">{sortInd("entryPrice")}</span></th>
              <th onClick={() => setSortKey("updatedTs")} className={cx("sortable", sort.key==="updatedTs" && "active")}>Updated <span className="sort-ind">{sortInd("updatedTs")}</span></th>
              <th onClick={() => setSortKey("lastAction")} className={cx("sortable", sort.key==="lastAction" && "active")}>Action <span className="sort-ind">{sortInd("lastAction")}</span></th>
              <th onClick={() => setSortKey("lastConfidence")} className={cx("sortable", sort.key==="lastConfidence" && "active")}>Conf <span className="sort-ind">{sortInd("lastConfidence")}</span></th>
              <th>Last alert</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            {sorted.flatMap(p => {
              const isExpanded = expanded === p.key;
              const lastEvent = p.events[p.events.length-1];
              const rcy = recencyBucket(p.updatedTs);
              const rows = [
                <tr key={p.key} className={cx(isExpanded && "expanded", !isExpanded && rcy === "live" && "row-fresh")} onClick={() => setExpanded(isExpanded ? null : p.key)}>
                  <td data-label="Analyst"><AnalystChip analyst={p.analyst} analysts={analysts} /></td>
                  <td data-label="Ticker"><span className="ticker">${p.ticker}</span></td>
                  <td data-label="Instrument"><span className="inst"><InstrumentTag event={p} /></span></td>
                  <td data-label="Expiry" className="mono" style={{ fontSize: "0.71875rem", whiteSpace: "nowrap" }}>{isOption(p) && p.expiry ? <span className="inst-detail">{fmtExpiry(p.expiry)}</span> : <span style={{ color: "var(--fg-3)" }}>—</span>}</td>
                  <td data-label="Strike" className="mono">{isOption(p) && p.strike != null ? <span className="inst-detail">{p.strike}</span> : <span style={{ color: "var(--fg-3)" }}>—</span>}</td>
                  <td data-label="Stop" className="mono">{p.stopLoss != null ? <span className="inst-detail" style={{ color: "var(--c-down)" }}>{fmtNum(p.stopLoss)}</span> : <span style={{ color: "var(--fg-3)" }}>—</span>}</td>
                  <td data-label="Entry → Now" className="num" style={{ whiteSpace: "nowrap" }}>
                    {(() => {
                      const basis = p.avgPrice != null ? p.avgPrice : p.entryPrice;
                      if (!basis) return "—";
                      return (
                        <>
                          {fmtNum(basis)}
                          {p.avgPrice != null && <span style={{ fontSize: "0.625rem", color: "var(--fg-3)", marginLeft: 3 }}>avg</span>}
                          {p.markPrice != null && (() => {
                            const up = p.markPrice >= basis;
                            const pnl = (p.markPrice - basis) / basis;
                            return (
                              <span style={{ color: up ? "var(--c-up)" : "var(--c-down)", marginLeft: 5 }}>
                                → {fmtNum(p.markPrice)} <span style={{ fontSize: "0.6875rem" }}>({fmtPct(pnl)})</span>
                              </span>
                            );
                          })()}
                        </>
                      );
                    })()}
                  </td>
                  <td data-label="Updated" className="muted mono" style={{ whiteSpace: "nowrap" }}>
                    {!isExpanded && rcy === "live" && <span className="rcy-new">NEW</span>}{fmtTime(p.updatedTs, { short: true })}
                  </td>
                  <td data-label="Action"><ActionPill kind={p.lastAction} /></td>
                  <td data-label="Conf"><ConfPill level={p.lastConfidence} /></td>
                  <td data-label="Last alert" className="muted cell-note" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}><span className="cell-text">{p.lastNote}</span></td>
                  <td className="cell-actions" style={{ textAlign: "right", whiteSpace: "nowrap" }}>
                    {onLogEvent && (!canManageRow || canManageRow(p)) && <button className="icon-btn manage-btn" title="Manage position — log a close / trim / add / update (admin)" onClick={(e) => { e.stopPropagation(); setManagePos(p); }}>{I("edit", { size: 15 })}<span className="btn-label">Manage</span></button>}
                    <button className="icon-btn" title="Cost calculator" onClick={(e) => { e.stopPropagation(); setCalcPos(p); }}>{I("calc", { size: 15 })}<span className="btn-label">Calculator</span></button>
                    {onOpenChart && <button className="icon-btn" title="Open chart" onClick={(e) => { e.stopPropagation(); onOpenChart(p.ticker); }}>{I("chart", { size: 15 })}<span className="btn-label">Chart</span></button>}
                  </td>
                </tr>
              ];
              if (isExpanded) {
                rows.push(
                  <tr key={p.key + "_x"} className="expand-row">
                    <td colSpan="12" style={{ padding: 0, borderBottom: "1px solid var(--border-2)" }}>
                      <div className="position-detail">
                        <h4 className="pd-title">All interactions · {p.events.length}</h4>
                        <TradeTimeline events={p.events} onEdit={canEditEvent && canEditEvent(p) ? onEditEvent : undefined} position={p} onEditPosition={canEditEvent && canEditEvent(p) ? onEditPosition : undefined} />
                      </div>
                      {showParser && <ParserDetail event={lastEvent} />}
                    </td>
                  </tr>
                );
              }
              return rows;
            })}
            {sorted.length === 0 && (
              <tr className="is-empty"><td colSpan="12"><Empty msg="No positions match this filter." /></td></tr>
            )}
          </tbody>
        </table>
        )}
      </div>
    </div>
    {calcPos && <PositionCalc position={calcPos} onClose={() => setCalcPos(null)} />}
    {managePos && <PositionManage position={managePos} onClose={() => setManagePos(null)} onSubmit={onLogEvent} />}
    {logOpen && <LogPositionModal analysts={analysts} servers={servers} onClose={() => setLogOpen(false)} onSubmit={onLogEvent} />}
    </>
  );
}

// The signature motif of the analyst-first landing: a slim segmented bar showing
// an analyst's book health — up / down / still-unmarked — proportioned by count.
// Unmarked stays a neutral segment (never implies a live mark it doesn't have),
// and semantic colours are never the analyst's identity accent. Reused full-size
// in each roster row and as an 88px mini on mobile.
function BookMeter({ up, down, count }) {
  const un = Math.max(0, count - up - down);
  const segs = [];
  if (up > 0) segs.push(["bm-up", up]);
  if (down > 0) segs.push(["bm-down", down]);
  if (un > 0) segs.push(["bm-un", un]);
  return (
    <span className="bmeter" role="img" aria-label={`${up} up, ${down} down, ${un} unmarked`}>
      {segs.map(([cls, v]) => <i key={cls} className={cls} style={{ flexGrow: v }} />)}
    </span>
  );
}

// Analyst-first landing (per-server setting `positions_by_analyst`): instead of a
// flat list of every open play, an EDITORIAL ROSTER — one full-bleed row per
// analyst (league-table idiom, no cards), ranked by open count, each showing
// their book health, avg stated P&L, holdings and what's expiring. Drill into
// one to see their positions. Fills the width by construction, so a 3-analyst
// book never reads as a sparse grid; reflows to a clean stack on mobile via CSS.
function AnalystOverview({ positions, analysts, onPick }) {
  const groups = useMemo(() => {
    const m = new Map();
    for (const p of positions) { const a = m.get(p.analyst) || []; a.push(p); m.set(p.analyst, a); }
    const out = [...m.entries()].map(([handle, ps]) => {
      const marks = ps.filter(x => x._pnl != null);
      const up = marks.filter(x => x._pnl >= 0).length;
      const down = marks.filter(x => x._pnl < 0).length;
      const avg = marks.length ? marks.reduce((s, x) => s + x._pnl, 0) / marks.length : null;
      let best = null, worst = null;
      for (const x of marks) { if (!best || x._pnl > best._pnl) best = x; if (!worst || x._pnl < worst._pnl) worst = x; }
      const expiring = ps.filter(x => isOption(x) && x.expiry && (() => { const d = dteOf(x.expiry); return d != null && d >= 0 && d <= 7; })()).length;
      // Distinct tickers, freshest-updated first — a peek at what they're holding.
      const seen = new Set(); const tickers = [];
      for (const x of [...ps].sort((a, b) => b.updatedTs - a.updatedTs)) { if (!seen.has(x.ticker)) { seen.add(x.ticker); tickers.push(x.ticker); } }
      const a = (analysts || []).find(x => x.handle === handle);
      return { handle, count: ps.length, up, down, avg, best, worst, expiring, tickers, color: a?.color || "var(--accent)", avatarUrl: a?.avatarUrl };
    });
    // Rank by activity; tie-breaks are deterministic (avg desc, then handle) so
    // the order and the "Most open" lede don't jitter between live polls.
    out.sort((x, y) => y.count - x.count || ((y.avg ?? -Infinity) - (x.avg ?? -Infinity)) || x.handle.localeCompare(y.handle));
    return out;
  }, [positions, analysts]);
  if (!groups.length) return <Empty msg="No open positions in this window." />;

  const n = groups.length;
  const sparse = n <= 4;               // a low count earns row height with real data, not padding
  const totalOpen = groups.reduce((a, g) => a + g.count, 0);
  const totalExp = groups.reduce((a, g) => a + g.expiring, 0);
  const allMarks = positions.filter(p => p._pnl != null);
  const bookAvg = allMarks.length ? allMarks.reduce((s, p) => s + p._pnl, 0) / allMarks.length : null;

  return (
    <div className={cx("an-roster", sparse && "sparse")}>
      <div className="an-roster-head" aria-hidden="true">
        <span></span><span>Analyst</span><span>Open</span><span className="r">Avg P&amp;L</span><span>Holdings</span><span></span><span></span>
      </div>
      {groups.map((g, idx) => {
        const marked = g.up + g.down;
        const unmarked = g.count - marked;
        const lead = sparse && idx === 0;
        const chips = g.tickers.slice(0, 5);
        const extra = g.tickers.length - chips.length;
        return (
          <button key={g.handle} type="button" className={cx("an-roster-row", lead && "lead")}
            data-tour={idx === 0 ? "analyst-overview" : undefined}
            style={{ "--col": g.color }} onClick={() => onPick(g.handle)} title={`${g.handle} — ${g.count} open`}>
            <span className="an-r-rank">{String(idx + 1).padStart(2, "0")}</span>
            <span className="an-r-id">
              {g.avatarUrl
                ? <DiscordAvatar url={g.avatarUrl} size={40} className="an-ov-pic" fallback={<span className="swatch lg" />} />
                : <span className="swatch lg" />}
              <span className="an-r-idtext">
                {lead && <span className="an-r-eyebrow">Most open</span>}
                <span className="an-r-name">{g.handle}</span>
                <span className="an-r-sub">
                  {marked === 0
                    ? <span className="muted">no marks yet</span>
                    : (sparse && g.best)
                      ? <>best <span className="up">{g.best.ticker} {fmtPct(g.best._pnl)}</span>{g.worst && g.worst !== g.best && <> · worst <span className="down">{g.worst.ticker} {fmtPct(g.worst._pnl)}</span></>}</>
                      : <>{g.up}<span className="up"> ▲</span> · {g.down}<span className="down"> ▼</span>{unmarked > 0 && <> · {unmarked} unmarked</>}</>}
                </span>
              </span>
            </span>
            <span className="an-r-book">
              <span className="an-r-open"><strong>{g.count}</strong> <span className="an-r-open-l">open</span></span>
              <BookMeter up={g.up} down={g.down} count={g.count} />
            </span>
            <span className="an-r-pnl">
              <span className={cx("fig", g.avg == null ? "none" : g.avg >= 0 ? "up" : "down")}>{g.avg == null ? "—" : fmtPct(g.avg)}</span>
              <span className="lbl">avg</span>
            </span>
            <span className="an-r-watch">
              {chips.map((t, i) => <React.Fragment key={t}>{i > 0 && <span className="dot">·</span>}{t}</React.Fragment>)}
              {extra > 0 && <span className="more"> +{extra}</span>}
            </span>
            <span className="an-r-flags">
              {g.expiring > 0 && <span className="an-r-exp">{g.expiring} exp</span>}
            </span>
            <span className="an-r-chev">{I("chevRight", { size: 16 })}</span>
          </button>
        );
      })}
      <div className="an-roster-foot">
        <span>{n} analyst{n === 1 ? "" : "s"} · {totalOpen} open{bookAvg != null && <> · avg <span className={bookAvg >= 0 ? "up" : "down"}>{fmtPct(bookAvg)}</span></>}{totalExp > 0 && <> · {totalExp} expiring this week</>}</span>
      </div>
    </div>
  );
}

function Positions({ positions, events, analysts, servers, filter, days, analystFirst, onPickTicker, onOpenChart, onLogEvent, canManageRow, showParser, onEditEvent, canEditEvent, onEditPosition }) {
  const isMobile = useIsMobile();
  const [sort, setSort] = useState({ key: "updatedTs", dir: "desc" });
  const [expanded, setExpanded] = useState(null);
  const [inst, setInst] = useState("all");
  const [anFilter, setAnFilter] = useState(() => new Set());
  const [seg, setSeg] = useState("all");   // all | winners | losers | today
  const [calcPos, setCalcPos] = useState(null);
  const [managePos, setManagePos] = useState(null);
  const [logOpen, setLogOpen] = useState(false);

  const cutoff = Date.now() - days * 86400000;
  // "Today" = the current MARKET day, not the viewer's calendar day — otherwise
  // a Malaysian morning would call the previous NY session "today".
  const todayKey = marketToday();
  const todayStart = (() => { const d = marketDayDate(todayKey); return d ? d.getTime() : 0; })();

  // Analysts that actually have an open position in range — populates the filter.
  const availableAnalysts = useMemo(() => {
    const set = new Set();
    for (const p of positions) {
      if (p.status === "closed" || p.status === "expired") continue;
      if (p.updatedTs < cutoff) continue;
      if (!matchesInstrument(p, inst)) continue;
      set.add(p.analyst);
    }
    return [...set].sort();
  }, [positions, cutoff, inst]);

  // Open positions in range, decorated with current P&L (the at-a-glance anchor).
  const decorated = useMemo(() => {
    const fl = filter.toLowerCase().trim();
    return positions
      .filter(p => p.status !== "closed" && p.status !== "expired")
      .filter(p => p.updatedTs >= cutoff)
      .filter(p => matchesInstrument(p, inst))
      .filter(p => analystMatch(p.analyst, anFilter))
      .filter(p => !fl || p.ticker.toLowerCase().includes(fl))
      .map(p => { const { basis, mark, pnl } = posPnl(p); return { ...p, _basis: basis, _mark: mark, _pnl: pnl }; });
  }, [positions, filter, cutoff, inst, anFilter]);

  // Quick segments — Winners / Losers / Today (Today = the current MARKET day).
  // Compare market-day KEYS, never a timestamp against a local-midnight anchor:
  // the anchor sits at a different instant than the trading day it names.
  const isToday = (p) => marketDayKey(p.updatedTs) === todayKey;
  const segCounts = useMemo(() => ({
    all: decorated.length,
    winners: decorated.filter(p => p._pnl != null && p._pnl >= 0).length,
    losers: decorated.filter(p => p._pnl != null && p._pnl < 0).length,
    today: decorated.filter(isToday).length,
  }), [decorated, todayKey]);

  const segFiltered = useMemo(() => {
    if (seg === "winners") return decorated.filter(p => p._pnl != null && p._pnl >= 0);
    if (seg === "losers") return decorated.filter(p => p._pnl != null && p._pnl < 0);
    if (seg === "today") return decorated.filter(isToday);
    return decorated;
  }, [decorated, seg, todayKey]);

  const sorted = useMemo(() => {
    const arr = [...segFiltered];
    const dir = sort.dir === "asc" ? 1 : -1;
    arr.sort((a, b) => {
      const av = a[sort.key], bv = b[sort.key];
      if (sort.key === "_pnl") {
        if (av == null && bv == null) return 0;
        if (av == null) return 1;            // unmarked sink to the bottom
        if (bv == null) return -1;
        return (av - bv) * dir;
      }
      if (typeof av === "number" && typeof bv === "number") return (av - bv) * dir;
      return String(av || "").localeCompare(String(bv || "")) * dir;
    });
    return arr;
  }, [segFiltered, sort]);

  function setSortKey(k) {
    setSort(s => s.key === k ? { ...s, dir: s.dir === "asc" ? "desc" : "asc" } : { key: k, dir: "desc" });
  }
  const sortInd = (k) => sort.key === k ? (sort.dir === "asc" ? "↑" : "↓") : "↕";

  // Analyst-first landing: overview (no analyst picked) vs drilled into one.
  const overviewMode = analystFirst && anFilter.size === 0;
  const drillAnalyst = analystFirst && anFilter.size === 1 ? [...anFilter][0] : null;
  const analystCount = new Set(decorated.map(p => p.analyst)).size;
  // A clear, contextual empty state — so a segment filtering everything out never
  // reads as a broken/blank page (the "All + Today shows nothing" confusion).
  const emptyMsg = seg === "today" ? "No open positions were active in today's market session."
    : seg === "winners" ? "No open positions are up right now."
    : seg === "losers" ? "No open positions are down right now."
    : "No open positions match this filter.";

  return (
    <>
    <div className="panel">
      <div className="panel-hdr">
        <div className="panel-title">
          {drillAnalyst ? (
            <span className="pos-drill-hdr">
              <button className="pos-back" onClick={() => setAnFilter(new Set())} title="Back to analysts"><span className="pos-back-arrow">←</span> Analysts</button>
              <span className="pos-drill-name">{drillAnalyst}</span>
            </span>
          ) : "Open Positions"}
        </div>
        <div className="panel-hdr-tools">
          {onLogEvent && <button className="btn primary btn-sm" onClick={() => setLogOpen(true)}>{I("plus", { size: 14 })} Log position</button>}
          {!overviewMode && (
            <div className="seg pos-seg" role="group" aria-label="Quick filter">
              {[["all", "All"], ["winners", "Winners"], ["losers", "Losers"], ["today", "Today"]].map(([v, l]) => (
                <button key={v} className={cx("seg-btn", seg === v && "on")} onClick={() => setSeg(v)}>
                  {l}{v !== "all" && segCounts[v] > 0 && <span className="seg-count">{segCounts[v]}</span>}
                </button>
              ))}
            </div>
          )}
          {!analystFirst && <AnalystFilter available={availableAnalysts} selected={anFilter} onChange={setAnFilter} analysts={analysts} />}
          <InstrumentFilter value={inst} onChange={setInst} />
          <div className="panel-meta">{overviewMode ? `${analystCount} analyst${analystCount === 1 ? "" : "s"}` : `${sorted.length} active`}</div>
        </div>
      </div>
      <div className="scroll-y feed-scroll">
        {overviewMode ? (
          <AnalystOverview positions={decorated} analysts={analysts} onPick={(h) => setAnFilter(new Set([h]))} />
        ) : sorted.length === 0 ? (
          <div className="pos-empty">
            <p>{emptyMsg}</p>
            {seg !== "all" && <button className="btn btn-sm" onClick={() => setSeg("all")}>Show all open positions</button>}
          </div>
        ) : isMobile ? (
          <MobilePositionList rows={sorted} analysts={analysts} onOpenChart={onOpenChart} onCalc={setCalcPos} onManage={onLogEvent ? setManagePos : undefined} canManageRow={canManageRow} showParser={showParser} onEditEvent={onEditEvent} canEditEvent={canEditEvent} onEditPosition={onEditPosition} />
        ) : (
        <table className="tbl tbl-fixed tbl-cards tbl-wide">
          <colgroup>
            <col style={{ width: "13%" }} />
            <col style={{ width: "9%" }} />
            <col style={{ width: "10%" }} />
            <col style={{ width: "17%" }} />
            <col style={{ width: "9%" }} />
            <col style={{ width: "12%" }} />
            <col style={{ width: "18%" }} />
            <col style={{ width: "12%" }} />
          </colgroup>
          <thead>
            <tr>
              <th onClick={() => setSortKey("analyst")} className={cx("sortable", sort.key==="analyst" && "active")}>Analyst <span className="sort-ind">{sortInd("analyst")}</span></th>
              <th onClick={() => setSortKey("ticker")} className={cx("sortable", sort.key==="ticker" && "active")}>Ticker <span className="sort-ind">{sortInd("ticker")}</span></th>
              <th>Instrument</th>
              <th onClick={() => setSortKey("_pnl")} className={cx("sortable num", sort.key==="_pnl" && "active")} style={{textAlign:"right"}}
                title={window.LEDGER_UI ? "Computed from the prices analysts stated in Discord — not market data" : undefined}>
                {window.LEDGER_UI ? <>P&amp;L · entry → last stated</> : <>P&amp;L · entry → now</>} <span className="sort-ind">{sortInd("_pnl")}</span>
              </th>
              <th onClick={() => setSortKey("lastAction")} className={cx("sortable", sort.key==="lastAction" && "active")}>Action <span className="sort-ind">{sortInd("lastAction")}</span></th>
              <th onClick={() => setSortKey("updatedTs")} className={cx("sortable", sort.key==="updatedTs" && "active")}>{window.LEDGER_UI ? "Last analyst update" : "Updated"} <span className="sort-ind">{sortInd("updatedTs")}</span></th>
              <th>Last alert</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            {sorted.flatMap(p => {
              const isExpanded = expanded === p.key;
              const lastEvent = p.events[p.events.length-1];
              const rcy = recencyBucket(p.updatedTs);
              const ledger = !!window.LEDGER_UI;
              const age = ledger ? markAge(p.updatedTs) : null;             // fresh | aging | stale
              const dte = ledger && isOption(p) ? dteOf(p.expiry) : null;   // contract math, no feed
              const anColor = (analysts.find(a => a.handle === p.analyst) || {}).color || "var(--accent)";
              const rows = [
                <tr key={p.key} className={cx(isExpanded && "expanded", p._pnl != null && (p._pnl >= 0 ? "pnl-up" : "pnl-down"), ledger && "pos-row-led")}
                  style={ledger ? { "--an-col": anColor } : undefined}
                  onClick={() => setExpanded(isExpanded ? null : p.key)}>
                  <td data-label="Analyst"><AnalystChip analyst={p.analyst} analysts={analysts} /></td>
                  <td data-label="Ticker">
                    <span className="ticker">${p.ticker}</span>
                    {ledger && <span className="pos-scent">{p.events.length} event{p.events.length === 1 ? "" : "s"}</span>}
                  </td>
                  <td data-label="Instrument">
                    <span className="inst"><InstrumentTag event={p} /></span>
                    {dte != null && <span className={cx("dte-chip", dteTier(dte))} title={p.expiry ? "Expires " + fmtExpiry(p.expiry) : undefined}>{dteLabel(dte)}</span>}
                    {isOption(p) && (p.strike != null || p.expiry) && (
                      <div className="inst-contract mono" title="Contract">
                        {p.strike != null && <span className="ic-strike">{strikeText(p)}</span>}
                        {p.strike != null && p.expiry && <span className="ic-sep"> · </span>}
                        {p.expiry && <span className="ic-exp">{fmtExpiry(p.expiry)}</span>}
                      </div>
                    )}
                  </td>
                  <td data-label="P&amp;L" className="num pos-pnl" style={{ whiteSpace: "nowrap" }} data-tour={p._lq ? "live-chart" : undefined}>
                    {p._pnl != null ? (
                      <>
                        {/* P&L% swaps INSTANTLY in steady sign colour — flashing a
                            derived number doubles the motion and adds nothing the
                            mark flash doesn't. The arrow is PERSISTENT and coloured
                            by SIGN, so direction is never colour-only (WCAG 1.4.1). */}
                        <span className={cx("pnl-big", p._pnl >= 0 ? "up" : "down")}>
                          {p._lq && <span className={cx("lv-arrow", p._pnl >= 0 ? "up" : "down")} aria-hidden="true">{p._pnl >= 0 ? "▲" : "▼"}</span>}
                          {fmtPct(p._pnl)}
                        </span>
                        <span className="pnl-sub mono">
                          {fmtPrice(p._basis, p.currency)}{p.avgPrice != null && <span className="pnl-avg">avg</span>} <span className="pnl-arr">→</span> <LiveNum q={p._lq}>{fmtPrice(p._mark, p.currency)}</LiveNum>
                          {p._lq
                            ? <LiveChip meta={liveQuoteMeta()} />
                            : ledger && <span className={cx("mark-age", age)} title={"Last stated by the analyst " + fmtTime(p.updatedTs)}>· {fmtAgo(p.updatedTs)}</span>}
                          {/* Option premium stays analyst-stated; show the underlying's live move as context. */}
                          {!p._lq && p._lqUnderlying && (
                            <span className="lv-underlying" title={"Underlying " + p.ticker + " live"} data-tour="live-underlying">
                              {" "}· {p.ticker} <LiveNum q={p._lqUnderlying}>{fmtNum(p._lqUnderlying.price)}</LiveNum>
                              <span className={cx("lv-u-arrow", p._lqUnderlying.dir >= 0 ? "up" : "down")}>{p._lqUnderlying.dir >= 0 ? "▲" : "▼"}</span>
                            </span>
                          )}
                        </span>
                        {p._lq && <LiveSpark hist={liveSparkHist(p.ticker)} up={p._pnl >= 0} />}
                      </>
                    ) : (p._basis != null
                      ? <span className="pnl-sub mono">{fmtPrice(p._basis, p.currency)} <span style={{ color: "var(--fg-3)" }}>entry</span>{ledger && <span className="mark-age none">· no mark yet</span>}</span>
                      : <span style={{ color: "var(--fg-3)" }}>—</span>)}
                  </td>
                  <td data-label="Action"><ActionPill kind={p.lastAction} /></td>
                  <td data-label="Updated" className={cx("muted mono", ledger && age === "stale" && "upd-stale")} style={{ whiteSpace: "nowrap" }}>
                    {!isExpanded && rcy === "live" && <span className="rcy-new">NEW</span>}{fmtTime(p.updatedTs, { short: true })}
                  </td>
                  <td data-label="Last alert" className="muted cell-note" style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}><span className="cell-text">{p.lastNote}</span></td>
                  <td className="cell-actions" style={{ textAlign: "right", whiteSpace: "nowrap" }}>
                    {onLogEvent && (!canManageRow || canManageRow(p)) && <button className="icon-btn manage-btn" title="Manage position — log a close / trim / add / update (admin)" onClick={(e) => { e.stopPropagation(); setManagePos(p); }}>{I("edit", { size: 15 })}<span className="btn-label">Manage</span></button>}
                    <button className="icon-btn" title="Cost calculator" onClick={(e) => { e.stopPropagation(); setCalcPos(p); }}>{I("calc", { size: 15 })}<span className="btn-label">Calculator</span></button>
                    {onOpenChart && <button className="icon-btn" title="Open chart" onClick={(e) => { e.stopPropagation(); onOpenChart(p.ticker); }}>{I("chart", { size: 15 })}<span className="btn-label">Chart</span></button>}
                  </td>
                </tr>
              ];
              if (isExpanded) {
                rows.push(
                  <tr key={p.key + "_x"} className="expand-row">
                    <td colSpan="8" style={{ padding: 0, borderBottom: "1px solid var(--border-2)" }}>
                      <div className="position-detail">
                        <dl className="pos-detail-grid">
                          <div><dt>Action</dt><dd><ActionPill kind={p.lastAction} /></dd></div>
                          <div><dt>Conf</dt><dd><ConfPill level={p.lastConfidence} /></dd></div>
                          {isOption(p) && <div><dt>Expiry</dt><dd className="mono">{p.expiry ? fmtExpiry(p.expiry) : "—"}</dd></div>}
                          {isOption(p) && <div><dt>Strike</dt><dd className="mono">{p.strike != null ? p.strike : "—"}</dd></div>}
                          <div><dt>Stop</dt><dd className="mono">{p.stopLoss != null ? <span style={{ color: "var(--c-down)" }}>{fmtPrice(p.stopLoss, p.currency)}</span> : "—"}</dd></div>
                          <div><dt>Entry</dt><dd className="mono">{p._basis != null ? <>{fmtPrice(p._basis, p.currency)}{p.avgPrice != null && <span className="pnl-avg">avg</span>}</> : "—"}</dd></div>
                          <div><dt>{window.LEDGER_UI ? "Last stated" : "Mark"}</dt><dd className="mono">
                            {p._mark != null ? fmtPrice(p._mark, p.currency) : "—"}
                            {window.LEDGER_UI && p._mark != null && <span className={cx("mark-age", markAge(p.updatedTs))}>· {fmtAgo(p.updatedTs)}</span>}
                          </dd></div>
                          {window.LEDGER_UI && isOption(p) && dteOf(p.expiry) != null && (
                            <div><dt>Time left</dt><dd className="mono"><span className={cx("dte-chip", dteTier(dteOf(p.expiry)))}>{dteLabel(dteOf(p.expiry))}</span></dd></div>
                          )}
                        </dl>
                        {window.LEDGER_UI && <div className="pd-provenance">Every price here is a mark the analyst stated in Discord — not market data.</div>}
                        <h4 className="pd-title">All interactions · {p.events.length}</h4>
                        <TradeTimeline events={p.events} onEdit={canEditEvent && canEditEvent(p) ? onEditEvent : undefined} position={p} onEditPosition={canEditEvent && canEditEvent(p) ? onEditPosition : undefined} />
                      </div>
                      {showParser && <ParserDetail event={lastEvent} />}
                    </td>
                  </tr>
                );
              }
              return rows;
            })}
            {sorted.length === 0 && (
              <tr className="is-empty"><td colSpan="8"><Empty msg="No positions match this filter." /></td></tr>
            )}
          </tbody>
        </table>
        )}
      </div>
    </div>
    {calcPos && <PositionCalc position={calcPos} onClose={() => setCalcPos(null)} />}
    {managePos && <PositionManage position={managePos} onClose={() => setManagePos(null)} onSubmit={onLogEvent} />}
    {logOpen && <LogPositionModal analysts={analysts} servers={servers} onClose={() => setLogOpen(false)} onSubmit={onLogEvent} />}
    </>
  );
}

// Cost calculator: contracts → cost, or budget → whole contracts.
function PositionCalc({ position, onClose }) {
  const isOpt = isOption(position);
  const mult = isOpt ? 100 : 1;
  const unitOne = isOpt ? "contract" : "share";
  const unitN = (n) => `${n} ${isOpt ? "contract" : "share"}${n === 1 ? "" : "s"}`;
  const basePrice = position.markPrice ?? position.avgPrice ?? position.entryPrice ?? position.lastPrice ?? 0;
  const [price, setPrice] = useState(basePrice ? String(basePrice) : "");
  const [qty, setQty] = useState("");
  const [budget, setBudget] = useState("");
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);

  const p = Number(price);
  const valid = isFinite(p) && p > 0;
  const perUnit = valid ? p * mult : 0;

  const nQty = Math.max(0, Math.floor(Number(qty) || 0));
  const qtyCost = perUnit * nQty;

  const bud = Number(budget) || 0;
  const buyable = valid && bud > 0 ? Math.floor(bud / perUnit) : 0;
  const buyableCost = buyable * perUnit;
  const leftover = bud - buyableCost;

  return (
    <div className="modal-back" onMouseDown={onClose}>
    <div className="modal calc-modal" role="dialog" aria-modal="true" onMouseDown={(e) => e.stopPropagation()}>
      <div className="calc-hdr">
        <div>
          <div className="calc-title"><span className="ticker">${position.ticker}</span> <span className="calc-contract mono">{contractText(position) || (isOpt ? "option" : "shares")}</span></div>
          <div className="calc-sub mono">{isOpt ? "1 contract = 100 shares" : "share calculator"}</div>
        </div>
        <button className="icon-btn" title="Close" onClick={onClose}>{I("close", { size: 16 })}</button>
      </div>

      <div className="calc-body">
        <label className="calc-field">
          <span>Price (per share)</span>
          <div className="calc-input"><span className="calc-pre">$</span><input type="number" min="0" step="0.01" value={price} onChange={(e) => setPrice(e.target.value)} placeholder="0.00" autoFocus /></div>
        </label>
        {valid
          ? <div className="calc-note">1 {unitOne} = <strong>{fmtCurrency(perUnit)}</strong>{isOpt && <> ({fmtNum(p)} × 100)</>}</div>
          : <div className="calc-warn">Enter a price to calculate.</div>}

        <div className="calc-split">
          <div className="calc-card">
            <div className="calc-card-hdr">{isOpt ? "Contracts" : "Shares"} → cost</div>
            <div className="calc-input"><input type="number" min="0" step="1" value={qty} onChange={(e) => setQty(e.target.value)} placeholder="0" /><span className="calc-suf">{isOpt ? "ct" : "sh"}</span></div>
            <div className="calc-out">
              <span>Total cost</span>
              <strong>{valid && nQty > 0 ? fmtCurrency(qtyCost) : "—"}</strong>
            </div>
          </div>

          <div className="calc-card">
            <div className="calc-card-hdr">Budget → {isOpt ? "contracts" : "shares"}</div>
            <div className="calc-input"><span className="calc-pre">$</span><input type="number" min="0" step="1" value={budget} onChange={(e) => setBudget(e.target.value)} placeholder="0" /></div>
            <div className="calc-out">
              {valid && bud > 0
                ? (buyable > 0
                    ? <><span>{unitN(buyable)}</span><strong>{fmtCurrency(buyableCost)}</strong></>
                    : <span className="calc-warn">Not within budget — 1 {unitOne} is {fmtCurrency(perUnit)}.</span>)
                : <><span>Whole {isOpt ? "contracts" : "shares"}</span><strong>—</strong></>}
            </div>
            {valid && buyable > 0 && <div className="calc-left mono">{fmtCurrency(leftover)} left over</div>}
          </div>
        </div>
      </div>
    </div>
    </div>
  );
}

// Admin-only: manually log a position update (close / trim / add / cut / update)
// from the portal. Posts a real event attributed to the position's analyst, so it
// flows into positions, recaps and charts exactly like a parsed Discord post.
function PositionManage({ position, onClose, onSubmit }) {
  const p = position;
  const opt = isOption(p);
  const basis = p.avgPrice != null ? p.avgPrice : p.entryPrice;
  const ACTIONS = [
    { k: "add",   label: "Add",    hint: "Bought more — adds to the position." },
    { k: "trim",  label: "Trim",   hint: "Sold part — banks profit, keeps runners open." },
    { k: "close", label: "Close",  hint: "Full exit — moves it to recaps." },
    { k: "cut",   label: "Cut",    hint: "Stopped out — closes the position." },
    { k: "hold",  label: "Update", hint: "No buy or sell — just refresh the mark / stop." },
  ];
  const [action, setAction] = useState("hold");   // default to "Update" (refresh mark/stop), not Trim
  const [price, setPrice] = useState("");
  const [priceMode, setPriceMode] = useState("$");   // "$" fill price, or "%" gain from entry (auto-computes $)
  const [addMode, setAddMode] = useState("fill");    // for an Add: "fill" (price added at) or "avg" (new blended average)
  const [mark, setMark] = useState("");
  const [stop, setStop] = useState(p.stopLoss != null ? String(p.stopLoss) : "");
  const [note, setNote] = useState("");
  const [busy, setBusy] = useState(false);
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape" && !busy) onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [busy]);

  const cur = ACTIONS.find(a => a.k === action) || ACTIONS[0];
  const isExit = action === "close" || action === "cut";
  const isUpdate = action === "hold";
  const priceLabel = action === "add" ? (addMode === "avg" ? "New average" : "Add fill price") : isExit ? "Exit price" : "Trim fill price";
  // A % gain only makes sense on a trim/exit (gain vs entry). For an add it's a new
  // entry, so keep $ only. When % is chosen, auto-compute the dollar fill from entry.
  const allowPct = action === "trim" || isExit;
  const effMode = allowPct ? priceMode : "$";
  const pctSign = (p.instrument === "shares" && p.direction === "short") ? -1 : 1;
  const computedFromPct = (effMode === "%" && basis != null && price !== "" && isFinite(Number(price)))
    ? basis * (1 + pctSign * Number(price) / 100)
    : null;

  async function submit() {
    if (busy) return;
    if (!p.guildId) { toast.error("This position has no server context — can't log."); return; }
    const addAvg = action === "add" && addMode === "avg";
    const numPrice = price !== "" ? Number(price) : null;
    const payload = {
      guild_id: p.guildId,
      analyst_handle: p.analyst,
      analyst_user_id: p.analystUserId,
      ticker: p.ticker,
      action,
      instrument: p.instrument,
      contract: p.contract,
      strike: p.strike,
      expiry: p.expiry,
      option_type: p.optionType,
      direction: p.direction,
      // In "New avg" mode the input IS the new blended average (→ avg_price), so
      // the cost basis updates — an add can't re-blend on its own without share
      // counts. Otherwise it's the fill price, exactly as before.
      price: addAvg ? null
        : effMode === "%" ? (computedFromPct != null ? Number(computedFromPct.toFixed(4)) : null)
        : numPrice,
      avg_price: addAvg ? numPrice : null,
      current_price: mark !== "" ? Number(mark) : null,
      stop_loss: stop !== "" ? Number(stop) : null,
      note: note.trim() || null,
    };
    setBusy(true);
    const ok = await onSubmit(payload);
    setBusy(false);
    if (ok) onClose();
  }

  return (
    <div className="modal-back" onMouseDown={() => !busy && onClose()}>
    <div className="modal manage-modal" role="dialog" aria-modal="true" onMouseDown={(e) => e.stopPropagation()}>
      <div className="calc-hdr">
        <div>
          <div className="calc-title"><span className="ticker">${p.ticker}</span> <span className="calc-contract mono">{contractText(p) || (opt ? "option" : "shares")}</span></div>
          <div className="calc-sub mono">Log a manual update · {p.analyst}</div>
        </div>
        <button className="icon-btn" title="Close" onClick={onClose}>{I("close", { size: 16 })}</button>
      </div>

      <div className="manage-body">
        <div className="manage-ctx mono">
          <span>entry <strong>{basis != null ? fmtNum(basis) : "—"}</strong></span>
          <span>mark <strong>{p.markPrice != null ? fmtNum(p.markPrice) : "—"}</strong></span>
          <span>stop <strong>{p.stopLoss != null ? fmtNum(p.stopLoss) : "—"}</strong></span>
        </div>

        <div className="manage-actions seg">
          {ACTIONS.map(a => (
            <button key={a.k} className={cx("seg-btn", action === a.k && "on")}
              style={action === a.k ? { color: `var(--c-${a.k})`, background: `color-mix(in oklab, var(--c-${a.k}) 16%, transparent)` } : undefined}
              onClick={() => setAction(a.k)}>{a.label}</button>
          ))}
        </div>
        <div className="manage-hint">{cur.hint}</div>

        <div className="manage-fields">
          {!isUpdate && (
            <label className="calc-field">
              <span className="calc-field-hdr">
                {effMode === "%" ? "Gain %" : priceLabel}{action === "add" ? "" : " (optional)"}
                {allowPct && (
                  <span className="mode-toggle" role="group" aria-label="Fill input mode">
                    <button type="button" aria-pressed={effMode === "$"} aria-label="Enter a dollar price" className={cx(effMode === "$" && "on")} onClick={(e) => { e.preventDefault(); setPriceMode("$"); }}>$</button>
                    <button type="button" aria-pressed={effMode === "%"} aria-label="Enter a percent gain" className={cx(effMode === "%" && "on")} onClick={(e) => { e.preventDefault(); setPriceMode("%"); }}>%</button>
                  </span>
                )}
                {action === "add" && (
                  <span className="mode-toggle" role="group" aria-label="Add input mode">
                    <button type="button" aria-pressed={addMode === "fill"} aria-label="Enter the price you added at" className={cx(addMode === "fill" && "on")} onClick={(e) => { e.preventDefault(); setAddMode("fill"); setPrice(""); }}>Fill</button>
                    <button type="button" aria-pressed={addMode === "avg"} aria-label="Enter your new blended average" className={cx(addMode === "avg" && "on")} onClick={(e) => { e.preventDefault(); setAddMode("avg"); setPrice(""); }}>New avg</button>
                  </span>
                )}
              </span>
              <div className="calc-input"><span className="calc-pre">{effMode === "%" ? "%" : "$"}</span><input type="number" step={effMode === "%" ? "0.1" : "0.01"} value={price} onChange={e => setPrice(e.target.value)} placeholder={effMode === "%" ? "16" : "0.00"} autoFocus /></div>
              {effMode === "%" && (
                computedFromPct != null
                  ? <span className="calc-note mono">= ${fmtNum(computedFromPct)} from entry {basis != null ? fmtNum(basis) : "—"}</span>
                  : (price !== "" && basis == null) ? <span className="calc-note mono" style={{ color: "var(--c-down)" }}>no entry on record — enter a $ price instead</span> : null
              )}
              {action === "add" && addMode === "avg" && (
                <span className="calc-note mono">Sets the new cost basis — we don't track share counts, so an add can't average down on its own.</span>
              )}
            </label>
          )}
          {(action === "add" || action === "trim" || isUpdate) && (
            <label className="calc-field">
              <span>Current mark (optional)</span>
              <div className="calc-input"><span className="calc-pre">$</span><input type="number" min="0" step="0.01" value={mark} onChange={e => setMark(e.target.value)} placeholder="0.00" /></div>
            </label>
          )}
          <label className="calc-field">
            <span>Stop loss (optional)</span>
            <div className="calc-input"><span className="calc-pre">$</span><input type="number" min="0" step="0.01" value={stop} onChange={e => setStop(e.target.value)} placeholder="—" /></div>
          </label>
        </div>

        <label className="calc-field">
          <span>Note (optional)</span>
          <textarea className="manage-note" rows={2} value={note} onChange={e => setNote(e.target.value)} placeholder={`e.g. ${cur.label.toLowerCase()} into strength`} />
        </label>

        <div className="manage-foot">
          <button className="btn" onClick={onClose} disabled={busy}>Cancel</button>
          <button className={cx("btn", isExit ? "danger" : "primary")} onClick={submit} disabled={busy}>
            {busy ? "Saving…" : `${cur.label} position`}
          </button>
        </div>
        <div className="manage-disclaimer">Adds a real event to ${p.ticker}'s history — updates positions, recaps & charts, and is recorded in the audit log.</div>
      </div>
    </div>
    </div>
  );
}

// Admin-only: open a brand-new position from the portal. Pick the analyst to
// attribute it to, the ticker + instrument, and an entry — posts an 'open' event
// down the same path as a parsed Discord call.
// Forgiving loose date parse → {y,m,d} (local calendar) or null. A posted-at is
// always in the PAST, so a year-less date that would land in the future rolls
// back a year ("12/30" typed in January = last December).
function parseLooseDate(s) {
  const t = String(s || "").trim();
  if (!t) return null;
  let m;
  if ((m = t.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/))) {
    const mo = +m[2], da = +m[3];
    return (mo >= 1 && mo <= 12 && da >= 1 && da <= 31) ? { y: +m[1], m: mo, d: da } : null;
  }
  if ((m = t.match(/^(\d{1,2})[/.-](\d{1,2})(?:[/.-](\d{2,4}))?$/))) {
    const mo = +m[1], da = +m[2];
    if (mo < 1 || mo > 12 || da < 1 || da > 31) return null;
    let y = m[3] != null ? +m[3] : null;
    if (y != null && y < 100) y += 2000;
    if (y == null) {
      const now = new Date();
      y = now.getFullYear();
      const cand = new Date(y, mo - 1, da).setHours(0, 0, 0, 0);
      if (cand > new Date().setHours(0, 0, 0, 0)) y -= 1;
    }
    return { y, m: mo, d: da };
  }
  return null;
}
// Forgiving loose time parse → {h,mm} 24h or null. Accepts "3:47pm", "3pm",
// "15:47", "347", "1547".
function parseLooseTime(s) {
  let t = String(s || "").trim().toLowerCase().replace(/\s+/g, "");
  if (!t) return null;
  const ap = /(am|pm)$/.exec(t);
  const mer = ap ? ap[1] : null;
  if (ap) t = t.slice(0, -2);
  let h, mm, m;
  if ((m = t.match(/^(\d{1,2}):(\d{2})$/))) { h = +m[1]; mm = +m[2]; }
  else if ((m = t.match(/^(\d{1,2})(\d{2})$/))) { h = +m[1]; mm = +m[2]; }
  else if ((m = t.match(/^(\d{1,2})$/))) { h = +m[1]; mm = 0; }
  else return null;
  if (mer === "pm" && h < 12) h += 12;
  if (mer === "am" && h === 12) h = 0;
  return (h <= 23 && mm <= 59) ? { h, mm } : null;
}
const _pad2 = (n) => String(n).padStart(2, "0");
const _wallStr = (v) => /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/.exec(v || "");
const _dateDisplay = (m) => `${+m[2]}/${+m[3]}/${m[1]}`;
const _timeDisplay = (m) => `${((+m[4] + 11) % 12) + 1}:${m[5]} ${(+m[4]) >= 12 ? "PM" : "AM"}`;

// On-brand "posted at" control: two of our own styled fields (date + ET time)
// composing the `YYYY-MM-DDTHH:MM` string the rest of the code already expects —
// no native OS picker. Live-echoes which trading session the time lands in.
function PostedAtField({ value, onChange }) {
  const init = _wallStr(value);
  const [date, setDate] = useState(init ? _dateDisplay(init) : "");
  const [time, setTime] = useState(init ? _timeDisplay(init) : "");

  function compose(d, t) {
    const pd = parseLooseDate(d), pt = parseLooseTime(t);
    onChange(pd && pt ? `${pd.y}-${_pad2(pd.m)}-${_pad2(pd.d)}T${_pad2(pt.h)}:${_pad2(pt.mm)}` : "");
  }
  function fillNow() {
    const w = epochToEtWallClock(Date.now());
    const m = _wallStr(w);
    if (!m) return;
    setDate(_dateDisplay(m)); setTime(_timeDisplay(m)); onChange(w);
  }
  function clear() { setDate(""); setTime(""); onChange(""); }

  const epoch = value ? etWallClockToEpoch(value) : null;
  return (
    <label className="calc-field">
      <span>Posted at · ET (optional)</span>
      <div className="postedat">
        <div className="calc-input postedat-date">
          <input type="text" value={date} placeholder="M/D/YYYY"
            onChange={e => { setDate(e.target.value); compose(e.target.value, time); }} />
        </div>
        <div className="calc-input postedat-time">
          <input type="text" value={time} placeholder="h:mm AM"
            onChange={e => { setTime(e.target.value); compose(date, e.target.value); }} />
        </div>
        <button type="button" className="btn btn-sm ghost" onClick={fillNow}>Now</button>
        {(date || time) && <button type="button" className="btn btn-sm ghost" onClick={clear}>Clear</button>}
      </div>
      <span className="calc-hint">
        {epoch != null
          ? <>lands on the <strong>{marketDayLabel(marketDayKey(epoch)).replace(/^Today · |^Yesterday · /, "")}</strong> session</>
          : (date || time)
            ? <>enter a date &amp; time, e.g. 7/17/2026 &amp; 3:47 PM</>
            : <>when the analyst actually called it — leave blank for now</>}
      </span>
    </label>
  );
}

function LogPositionModal({ analysts, servers, onClose, onSubmit }) {
  const roster = (analysts || []).filter(a => a.discord_user_id && a.guild_id);
  const srvName = (gid) => (servers || []).find(s => s.guild_id === gid)?.name || "";
  const multiSrv = new Set(roster.map(a => a.guild_id)).size > 1;
  const aKey = (a) => a.guild_id + "|" + a.discord_user_id;
  const [analystKey, setAnalystKey] = useState(roster[0] ? aKey(roster[0]) : "");
  const analyst = roster.find(a => aKey(a) === analystKey) || roster[0] || null;
  const [ticker, setTicker] = useState("");
  const [kind, setKind] = useState("option");   // option | shares
  const [optType, setOptType] = useState("call"); // call | put
  const [dir, setDir] = useState("long");          // shares: long | short
  const [strike, setStrike] = useState("");
  const [expiry, setExpiry] = useState("");
  const [price, setPrice] = useState("");
  const [mark, setMark] = useState("");
  const [stop, setStop] = useState("");
  const [note, setNote] = useState("");
  const [postedAt, setPostedAt] = useState("");   // ET wall clock; blank = now
  const [busy, setBusy] = useState(false);
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape" && !busy) onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [busy]);

  const isOpt = kind === "option";

  async function submit() {
    if (busy) return;
    if (!analyst) { toast.error("Add an analyst first to attribute the position."); return; }
    const tk = ticker.trim().toUpperCase().replace(/^\$/, "");
    if (!tk) { toast.error("Enter a ticker."); return; }
    if (price === "" || !(Number(price) > 0)) { toast.error("Enter an entry price."); return; }
    if (isOpt && strike.trim() === "") { toast.error("Enter a strike for the option."); return; }
    // "Posted at" is the moment the ANALYST called it (ET), not when it was keyed
    // in — that's what decides which trading day the position belongs to.
    let ts = null;
    if (postedAt) {
      ts = etWallClockToEpoch(postedAt);
      if (ts == null) { toast.error("Couldn't read that posted-at time."); return; }
      if (ts > Date.now() + 6 * 3600e3) { toast.error("Posted-at is in the future."); return; }
    }
    const contract = isOpt
      ? `${expiry.trim() ? expiry.trim() + " " : ""}${strike.trim()}${optType === "put" ? "P" : "C"}`.trim()
      : null;
    const payload = {
      guild_id: analyst.guild_id,
      analyst_handle: analyst.handle,
      analyst_user_id: analyst.discord_user_id,
      ticker: tk,
      action: "open",
      instrument: isOpt ? "option" : "shares",
      contract,
      strike: isOpt && strike !== "" ? Number(strike) : null,
      expiry: isOpt ? (expiry.trim() || null) : null,
      option_type: isOpt ? optType : null,
      direction: isOpt ? (optType === "put" ? "short" : "long") : dir,
      price: Number(price),
      current_price: mark !== "" ? Number(mark) : null,
      stop_loss: stop !== "" ? Number(stop) : null,
      note: note.trim() || null,
      ...(ts != null ? { ts } : {}),
    };
    setBusy(true);
    const ok = await onSubmit(payload);
    setBusy(false);
    if (ok) onClose();
  }

  return (
    <div className="modal-back" onMouseDown={() => !busy && onClose()}>
    <div className="modal manage-modal" role="dialog" aria-modal="true" onMouseDown={(e) => e.stopPropagation()}>
      <div className="calc-hdr">
        <div>
          <div className="calc-title">{I("plus", { size: 16 })} Log a position</div>
          <div className="calc-sub mono">Open a new position from the portal</div>
        </div>
        <button className="icon-btn" title="Close" onClick={onClose}>{I("close", { size: 16 })}</button>
      </div>

      <div className="manage-body">
        {roster.length === 0 ? (
          <div className="calc-warn">No analysts are registered for your servers yet — add one in Settings → Analysts first.</div>
        ) : (
          <>
            <div className="manage-fields">
              <label className="calc-field">
                <span>Analyst</span>
                <select className="select" value={analystKey} onChange={e => setAnalystKey(e.target.value)}>
                  {roster.map(a => (
                    <option key={aKey(a)} value={aKey(a)}>{a.handle}{multiSrv && srvName(a.guild_id) ? ` · ${srvName(a.guild_id)}` : ""}</option>
                  ))}
                </select>
              </label>
              <label className="calc-field">
                <span>Ticker</span>
                <div className="calc-input"><span className="calc-pre">$</span><input value={ticker} onChange={e => setTicker(e.target.value)} placeholder="NVDA" autoFocus /></div>
              </label>
            </div>

            <div className="manage-twoseg">
              <div className="seg">
                <button className={cx("seg-btn", isOpt && "on")} onClick={() => setKind("option")}>Option</button>
                <button className={cx("seg-btn", !isOpt && "on")} onClick={() => setKind("shares")}>Shares</button>
              </div>
              {isOpt ? (
                <div className="seg">
                  <button className={cx("seg-btn", optType === "call" && "on")} onClick={() => setOptType("call")}>Call</button>
                  <button className={cx("seg-btn", optType === "put" && "on")} onClick={() => setOptType("put")}>Put</button>
                </div>
              ) : (
                <div className="seg">
                  <button className={cx("seg-btn", dir === "long" && "on")} onClick={() => setDir("long")}>Long</button>
                  <button className={cx("seg-btn", dir === "short" && "on")} onClick={() => setDir("short")}>Short</button>
                </div>
              )}
            </div>

            {isOpt && (
              <div className="manage-fields">
                <label className="calc-field">
                  <span>Strike</span>
                  <div className="calc-input"><span className="calc-pre">$</span><input type="number" min="0" step="0.5" value={strike} onChange={e => setStrike(e.target.value)} placeholder="210" /></div>
                </label>
                <label className="calc-field">
                  <span>Expiry</span>
                  <div className="calc-input"><input value={expiry} onChange={e => setExpiry(e.target.value)} placeholder="6/20 or 6/20/26" /></div>
                </label>
              </div>
            )}

            <div className="manage-fields">
              <label className="calc-field">
                <span>Entry price</span>
                <div className="calc-input"><span className="calc-pre">$</span><input type="number" min="0" step="0.01" value={price} onChange={e => setPrice(e.target.value)} placeholder="0.00" /></div>
              </label>
              <label className="calc-field">
                <span>Current mark (optional)</span>
                <div className="calc-input"><span className="calc-pre">$</span><input type="number" min="0" step="0.01" value={mark} onChange={e => setMark(e.target.value)} placeholder="0.00" /></div>
              </label>
              <PostedAtField value={postedAt} onChange={setPostedAt} />
              <label className="calc-field">
                <span>Stop loss (optional)</span>
                <div className="calc-input"><span className="calc-pre">$</span><input type="number" min="0" step="0.01" value={stop} onChange={e => setStop(e.target.value)} placeholder="—" /></div>
              </label>
            </div>

            <label className="calc-field">
              <span>Note (optional)</span>
              <textarea className="manage-note" rows={2} value={note} onChange={e => setNote(e.target.value)} placeholder="e.g. starter, breakout setup" />
            </label>

            <div className="manage-foot">
              <button className="btn" onClick={onClose} disabled={busy}>Cancel</button>
              <button className="btn primary" onClick={submit} disabled={busy}>{busy ? "Saving…" : "Open position"}</button>
            </div>
            <div className="manage-disclaimer">Creates a new open position attributed to {analyst ? analyst.handle : "the analyst"} — it appears in positions, recaps & charts, and is recorded in the audit log.</div>
          </>
        )}
      </div>
    </div>
    </div>
  );
}

function EventRail({ events, analysts, onClickEvent }) {
  return (
    <div className="panel">
      <div className="panel-hdr">
        <div className="panel-title">Latest Position Events</div>
        <div className="panel-meta">{events.length} parsed</div>
      </div>
      <div className="scroll-y">
        {events.slice(0, 60).map((e, i) => (
          <div key={e.id} className={cx("event", recencyBucket(e.ts) === "live" && "event-fresh", i === 0 && e.__fresh && "slide-in")} onClick={() => onClickEvent && onClickEvent(e)} style={onClickEvent ? undefined : { cursor: "default" }}>
            <div className="e-analyst" style={{ color: analysts.find(a => a.handle === e.analyst)?.color || "var(--accent)" }}>{e.analyst}</div>
            <div className="e-time">{fmtTime(e.ts, {short:true})}</div>
            <div className="e-ticker">${e.ticker}</div>
            <div className="e-pill"><ActionPill kind={e.action} /></div>
            <div className="e-note">{e.contract ? <span className="contract">{e.contract}</span> : null} {e.contract && <span style={{ color: "var(--fg-3)", margin: "0 6px" }}>·</span>}<span style={{ color: "var(--fg-1)" }}>{e.raw}</span></div>
          </div>
        ))}
      </div>
    </div>
  );
}

Object.assign(window, { Positions, PositionsClassic, PositionsSummary, EventRail, ParserDetail, TradeTimeline, PositionIdentityEdit });
