/* live-quotes.jsx — the REAL live-market overlay (ships to production).

   Polls OUR /api/quotes (vendor key stays server-side) for the underlying
   tickers of positions in servers the owner has switched Live Data on for, and
   overlays live marks onto those positions. Everything downstream — posPnl, the
   summary strip, sorting, colouring — ticks unchanged.

   Scope of what goes live (the honest hybrid):
   · SHARES position   → its own live price becomes the mark. Full live P&L.
   · OPTION position   → the option PREMIUM stays analyst-stated (real option
     marks need an OPRA-carrying data plan; when one is configured the server
     returns them and this fills in automatically). Meanwhile the UNDERLYING's
     live price rides along as context on the row.

   Presentation (from the platform-UX research): flash the changed NUMBER's text
   colour only, ~1Hz, persistent sign arrow, chip that reads LIVE / DELAYED per
   the vendor (or SIM while the server runs the simulated provider). All of it is
   inert until a server is switched on — zero cost, zero motion, otherwise. */

// Module store, outside React (one poll drives every subscriber).
const _lq = {
  quotes: new Map(),     // SYMBOL → { price, prevClose, prevPrice, dir, seq, ts, hist }
  bars: new Map(),       // SYMBOL → [intraday closes] (seeds the sparkline)
  subs: new Set(),
  meta: { enabled: false, provider: null, delayed: false },
  timer: null,
  barsAt: 0,             // last bars refresh
  symbols: [],           // the current watch list (underlyings of live positions)
  poll: 5000,
};

function _notify() { for (const fn of _lq.subs) { try { fn(); } catch (_) {} } }

async function _fetchOnce() {
  if (!_lq.symbols.length) { _lq.meta = { ..._lq.meta, enabled: _lq.meta.enabled }; return; }
  try {
    const r = await tapeFetch(apiBase() + "/api/quotes?symbols=" + encodeURIComponent(_lq.symbols.join(",")));
    const j = await r.json();
    _lq.meta = { enabled: !!j.enabled, provider: j.provider || null, delayed: !!j.delayed };
    let changed = false;
    for (const [sym, q] of Object.entries(j.quotes || {})) {
      if (q == null || q.price == null) continue;
      const prev = _lq.quotes.get(sym);
      const price = Number(q.price);
      const dir = prev ? (price > prev.price ? 1 : price < prev.price ? -1 : prev.dir) : 0;
      const hist = prev ? prev.hist.slice(-39) : [];
      if (!prev || prev.price !== price) hist.push(price);
      _lq.quotes.set(sym, {
        price, prevClose: q.prevClose ?? (prev && prev.prevClose) ?? null,
        prevPrice: prev ? prev.price : price, dir,
        seq: (prev ? prev.seq : 0) + (!prev || prev.price !== price ? 1 : 0),
        ts: q.ts || Date.now(), hist,
      });
      if (!prev || prev.price !== price) changed = true;
    }
    if (changed || _lq.meta.enabled) _notify();
  } catch (_) { /* hold last marks on a blip */ }
}

// Intraday bars seed each row's sparkline with a real price path immediately,
// instead of an empty line that fills tick by tick. Refreshed ~every 4 min.
async function _fetchBars() {
  if (!_lq.symbols.length) return;
  if (Date.now() - _lq.barsAt < 4 * 60 * 1000) return;
  _lq.barsAt = Date.now();
  try {
    const r = await tapeFetch(apiBase() + "/api/bars?symbols=" + encodeURIComponent(_lq.symbols.join(",")));
    const j = await r.json();
    let changed = false;
    for (const [sym, closes] of Object.entries(j.bars || {})) {
      if (Array.isArray(closes) && closes.length) { _lq.bars.set(sym, closes); changed = true; }
    }
    if (changed) _notify();
  } catch (_) { _lq.barsAt = 0; }   // retry next cycle on a blip
}

function _ensurePolling() {
  if (_lq.timer) return;
  _fetchBars();
  const tick = () => { _fetchOnce(); _fetchBars(); _lq.timer = setTimeout(tick, _lq.poll); };
  tick();
}
function _stopPolling() { if (_lq.timer) { clearTimeout(_lq.timer); _lq.timer = null; } }

// Called by app.jsx whenever positions/servers change: sets the watch list to
// the underlyings of positions in Live-Data-on servers, and starts/stops polling.
function liveQuotesWatch(positions, servers) {
  const liveGuilds = new Set((servers || []).filter(s => s.live_data === true).map(s => s.guild_id));
  if (!liveGuilds.size) { _lq.symbols = []; _stopPolling(); if (_lq.meta.enabled) { _lq.meta.enabled = false; _notify(); } return; }
  const syms = new Set();
  for (const p of positions) {
    if (p.status === "closed" || p.status === "expired") continue;
    if (!liveGuilds.has(p.guildId)) continue;
    const t = String(p.ticker || "").toUpperCase().trim();
    if (t) syms.add(t);
  }
  _lq.symbols = [...syms].sort();
  if (_lq.symbols.length) _ensurePolling(); else _stopPolling();
}

function useLiveQuotes() {
  const [, force] = useState(0);
  useEffect(() => {
    const fn = () => force(x => x + 1);
    _lq.subs.add(fn);
    return () => _lq.subs.delete(fn);
  }, []);
  return _lq.quotes;
}
function liveQuoteFor(ticker) { return _lq.quotes.get(String(ticker || "").toUpperCase().trim()) || null; }
function liveQuoteMeta() { return _lq.meta; }
// The sparkline path for a ticker: today's intraday bars, then the live ticks
// accumulated since — so it draws a real chart from the first frame.
function liveSparkHist(ticker) {
  const sym = String(ticker || "").toUpperCase().trim();
  const bars = _lq.bars.get(sym) || [];
  const q = _lq.quotes.get(sym);
  const ticks = q ? q.hist : [];
  return bars.concat(ticks).slice(-48);
}

// ── Presentation (shared) ────────────────────────────────────────────────────
// Flash the changed number's text colour. Re-keying the span on `seq` restarts
// the CSS animation for free. Disabled under prefers-reduced-motion (styles.css).
function LiveNum({ q, children }) {
  if (!q) return children;
  return (
    <span key={q.seq} className={cx("lv-flash", q.dir > 0 ? "lv-up" : q.dir < 0 ? "lv-down" : "")}>
      {children}
    </span>
  );
}
function LiveSpark({ hist, up }) {
  if (!hist || hist.length < 2) return null;
  const w = 84, h = 22;
  const min = Math.min(...hist), max = Math.max(...hist), span = max - min || 1;
  const pts = hist.map((v, i) =>
    `${(i / (hist.length - 1) * w).toFixed(1)},${(h - 2 - (v - min) / span * (h - 4)).toFixed(1)}`).join(" ");
  return (
    <svg className="lv-spark" width={w} height={h} viewBox={`0 0 ${w} ${h}`} aria-hidden="true">
      <polyline points={pts} fill="none" stroke={up ? "var(--c-up)" : "var(--c-down)"} strokeWidth="1.5" />
    </svg>
  );
}
// Data-state chip. Only genuine real-time is badged (green LIVE). Delayed / sim
// carry NO chip — the owner's call ("removed the delayed"); the flashing price
// and sparkline already read as live-ish. Flip back by returning the label here.
function LiveChip({ meta, title }) {
  if (!meta || meta.delayed || meta.provider === "sim") return null;
  return <span className="lv-chip lv-chip-live" title={title || "LIVE"}>LIVE</span>;
}

Object.assign(window, {
  liveQuotesWatch, useLiveQuotes, liveQuoteFor, liveQuoteMeta, liveSparkHist,
  LiveNum, LiveSpark, LiveChip,
});
