/* global React, ReactDOM, store, DashboardPage, ProposalsPage, NewsPage, ScannerPage, SymbolsPage, OptionsPage, BacktestPage, BotsPage, SettingsPage */
const { useState, useEffect, useMemo, useRef, useCallback } = React;

function Clock() {
  const [t, setT] = useState(new Date());
  // Re-render whenever user prefs change so time_format swap is live.
  const [, setBump] = useState(0);
  useEffect(() => {
    const i = setInterval(() => setT(new Date()), 1000);
    const onPrefs = () => setBump(b => b + 1);
    window.addEventListener("userprefs:changed", onPrefs);
    return () => { clearInterval(i); window.removeEventListener("userprefs:changed", onPrefs); };
  }, []);
  const fmt = (window.__userPrefs && window.__userPrefs.time_format) || "24h";
  return <span className="tnum">{t.toLocaleTimeString("en-IN", { hour12: fmt === "12h" })} IST</span>;
}

// Brand mark — Tulaker logo (gold scales + bull/bear + candlesticks).
// Served as a static PNG from /terminal/tulaker-logo.png (256px master,
// browser scales down). Same asset is used for the favicon.
function BrandMark({ size = 44 }) {
  // mix-blend-mode: lighten makes the logo's near-black background drop out
  // against the topbar gradient — only the gold artwork shows through.
  return (
    <img src="tulaker-logo.png" alt="Tulaker"
         width={size} height={size}
         style={{ display: "block", flexShrink: 0, objectFit: "contain",
                  mixBlendMode: "lighten" }}/>
  );
}

// Generic topbar pill for an external integration (TradingView / NSE / MCX / News).
// `conn` is the per-source object emitted by ui_state._connections_status:
//    { state: "live"|"ready"|"stale"|"offline", detail, last_at?, buffered? }
function ConnectionPill({ label, conn }) {
  const state = (conn && conn.state) || "offline";
  const cls =
    state === "live"  ? "pill-up"        :
    state === "ready" ? "pill-cyan"      :
    state === "stale" ? "pill-amber"     : "pill-dim";
  const text =
    state === "live"  ? "LIVE"           :
    state === "ready" ? "READY"          :
    state === "stale" ? "STALE"          : "OFFLINE";
  const dotCls =
    state === "live"  ? "dot-live"       :
    state === "ready" ? "dot-dim"        :
    state === "stale" ? "dot-dim"        : "dot-dim";
  return (
    <div className="topbar-stat" title={(conn && conn.detail) || `${label}: ${state}`}>
      <span className="h-xxs">{label}</span>
      <span className={`pill ${cls}`}>
        <span className={`dot ${dotCls}`} style={{ width: 5, height: 5 }}/>
        {text}
      </span>
    </div>
  );
}

// Ticker tape at the bottom
function TickerTape({ quotes }) {
  return (
    <div className="ticker-tape">
      <div className="ticker-track">
        {[...Object.values(quotes), ...Object.values(quotes)].map((q, i) => {
          const chg = q.last - q.price;
          const pct = (chg / q.price) * 100;
          const up = chg >= 0;
          return (
            <span key={i} className="ticker-item">
              <span className="bright">{q.sym}</span>
              <span className="tnum">{q.last.toFixed(2)}</span>
              <span className={`tnum ${up ? "up" : "down"}`}>{up ? "▲" : "▼"} {Math.abs(pct).toFixed(2)}%</span>
            </span>
          );
        })}
      </div>
    </div>
  );
}

function ClaudeDrawer({ open, onClose, app, quotes, onGoto }) {
  const [messages, setMessages] = useState([
    { role: "system", text: "Connected to MCP server · 23 tools available", t: Date.now() - 60000 },
    { role: "claude", text: "Good morning. I've scanned your watchlist and queued 3 proposals. RELIANCE looks most actionable — volume confirmed breakout. Want me to break it down?", t: Date.now() - 30000 },
  ]);
  const [input, setInput] = useState("");
  const listRef = useRef();
  useEffect(() => { if (listRef.current) listRef.current.scrollTop = listRef.current.scrollHeight; }, [messages, open]);

  const quickActions = [
    { label: "scan watchlist", prompt: "Scan my watchlist for entries" },
    { label: "show positions", prompt: "What are my current positions and P&L?" },
    { label: "news recap", prompt: "Give me the last 2 hours of news" },
    { label: "check risk", prompt: "What's my current risk usage?" },
  ];

  const send = (text) => {
    if (!text.trim()) return;
    const userMsg = { role: "user", text, t: Date.now() };
    setMessages(prev => [...prev, userMsg]);
    setInput("");
    setTimeout(() => {
      setMessages(prev => [...prev, { role: "tool", text: "→ calling bias_analysis, get_positions, news_recent…", t: Date.now() }]);
    }, 400);
    setTimeout(() => {
      setMessages(prev => [...prev, {
        role: "claude",
        text: claudeReply(text, app, quotes),
        t: Date.now(),
      }]);
    }, 1400);
  };

  return (
    <div className={`copilot ${open ? "open" : ""}`}>
      <div className="panel-header">
        <span className="violet" style={{ fontSize: 11 }}>◆</span>
        <span className="title">CLAUDE COPILOT</span>
        <span className="pill pill-up" style={{ fontSize: 9 }}>
          <span className="dot dot-live" style={{ width: 4, height: 4 }}/> CONNECTED
        </span>
        <div style={{ flex: 1 }}/>
        <button className="btn btn-xs btn-ghost" onClick={onClose}>✕</button>
      </div>
      <div className="copilot-body" ref={listRef}>
        {messages.map((m, i) => (
          <div key={i} className={`msg msg-${m.role}`}>
            {m.role === "claude" && <span className="violet" style={{ fontSize: 9, letterSpacing: "0.08em" }}>◆ CLAUDE</span>}
            {m.role === "tool" && <span className="cyan" style={{ fontSize: 9, letterSpacing: "0.08em" }}>⚙ TOOL</span>}
            {m.role === "system" && <span className="faint" style={{ fontSize: 9, letterSpacing: "0.08em" }}>SYSTEM</span>}
            {m.role === "user" && <span className="amber" style={{ fontSize: 9, letterSpacing: "0.08em" }}>YOU</span>}
            <div style={{ fontSize: 11, lineHeight: 1.55, marginTop: 2, whiteSpace: "pre-wrap" }}>{m.text}</div>
          </div>
        ))}
      </div>
      <div style={{ padding: 8, borderTop: "1px solid var(--border)" }}>
        <div style={{ display: "flex", flexWrap: "wrap", gap: 4, marginBottom: 6 }}>
          {quickActions.map(a => (
            <button key={a.label} className="chip" onClick={() => send(a.prompt)}>{a.label}</button>
          ))}
        </div>
        <form onSubmit={e => { e.preventDefault(); send(input); }} style={{ display: "flex", gap: 4 }}>
          <input className="input" placeholder="Ask Claude… (e.g., 'analyse RELIANCE')" value={input} onChange={e => setInput(e.target.value)} autoFocus={open}/>
          <button className="btn btn-primary" type="submit">↵</button>
        </form>
      </div>
    </div>
  );
}
function claudeReply(q, app, quotes) {
  const lower = q.toLowerCase();
  if (lower.includes("position")) {
    const lines = app.positions.map(p => `  ${p.symbol}  ${p.qty} @ ${p.avg.toFixed(2)}  · P&L ${p.pnl >= 0 ? "+" : ""}${p.pnl.toFixed(0)}`).join("\n");
    return `You have ${app.positions.length} open positions:\n${lines}\n\nNet open P&L: ${store.inrSigned(app.positions.reduce((s, p) => s + p.pnl, 0))}`;
  }
  if (lower.includes("risk")) {
    return `Risk usage:\n  · Day loss 0 / 2,000 (0%)\n  · Orders today 6 / 20\n  · Exposure ₹86,500 / ₹200,000 (43%)\n  · Regime ${app.regime}\n\nAll caps green. Room for ~14 more orders today.`;
  }
  if (lower.includes("news")) {
    return `Last 2h highlights:\n  · [HIGH] RELIANCE Q4 PAT beat by 8%\n  · [MED]  RBI holds repo at 6.25%, hawkish tone\n  · [LOW]  TCS wins $2.1B European bank deal\n\nNet bias: mildly bullish Nifty 50, with positive idiosyncratic drivers for RELIANCE and TCS.`;
  }
  if (lower.includes("scan")) {
    return `Scan complete across 18 symbols:\n  BULLISH (5): RELIANCE, HDFCBANK, BHARTIARTL, INFY, ICICIBANK\n  BEARISH (3): TCS (overbought), ASIANPAINT, KOTAKBANK\n  NEUTRAL (10)\n\nTop conviction: RELIANCE (+0.42). Already queued a proposal.`;
  }
  if (lower.includes("reliance")) {
    const q = quotes.RELIANCE;
    return `RELIANCE @ ${q?.last.toFixed(2) || "2890"}:\n  · 15m: broke 20-EMA, vol 1.4× avg\n  · RSI14 58 (bullish but not stretched)\n  · MACD hist +2.4, turning up\n  · ADX 27, trend building\n  · Bias score +0.42 (BULLISH)\n\nProposal cx_9f3a21 in queue · BUY 10 @ LIMIT 2890.45. Confirm it?`;
  }
  return `I can help with bias analysis, scanning, position review, news digests, risk checks, and drafting trade proposals. What would you like?`;
}

// ===== URL <-> internal page sync =====
//
// Pretty paths are first-class:  /scanner, /news, /options, etc.  The SPA
// reads window.location.pathname and shows the matching page.  Pages without
// a dedicated path (sectors, calendars, etc.) fall back to ?page=X — the SPA
// can still deep-link to them, just not via a clean URL.
//
// next.config.js has a catch-all rewrite that serves /terminal/index.html for
// every non-/api / non-/ws path, so the URL bar stays clean while the SPA
// stays the only mounted React tree on the page.
const _PATH_TO_PAGE = {
  "/":                   "dashboard",
  "/scanner":            "scanner",
  "/symbols":            "scanner_symbols",
  "/options":            "options",
  "/backtest":           "backtest",
  "/news":               "news",
  "/bots":               "bots",
  "/council":            "council",
  "/trade-council":      "council_trade",
  "/investment-council": "council_investment",
  "/proposals":          "council_proposals",
  "/arbitrage":          "arbitrage",
  "/settings":           "settings",
  "/apikeys":            "apikeys",
  "/sectors":            "sectors",
  "/calendars":          "calendars",
  "/global":             "global",
  "/history":            "council_history",
};
const _PAGE_TO_PATH = Object.fromEntries(
  Object.entries(_PATH_TO_PAGE).map(([path, page]) => [page, path])
);

function _initialPageFromUrl() {
  try {
    const u = new URL(window.location.href);
    if (_PATH_TO_PAGE[u.pathname]) return _PATH_TO_PAGE[u.pathname];
    // Legacy / SPA-only pages still recognised via ?page=
    return u.searchParams.get("page") || "dashboard";
  } catch (e) { return "dashboard"; }
}
function _syncPageToUrl(p) {
  try {
    const path = _PAGE_TO_PATH[p];
    if (path) {
      // Clean URL — pushState so back/forward works; replaces ?page= query.
      window.history.pushState(null, "", path);
    } else {
      // Fallback for pages without a mapped path: keep ?page= for deep-link.
      const u = new URL(window.location.href);
      u.searchParams.set("page", p);
      window.history.pushState(null, "", u.toString());
    }
  } catch (e) {}
}

// ===== MAIN APP =====
async function _signOut() {
  try {
    await fetch("/api/auth/logout", { method: "POST", credentials: "include" });
  } catch (_e) {}
  window.location.href = "/";
}

// ===== USER PREFERENCES =====
// Map accent_color enum values → CSS colour. Kept in JS so the
// runtime can swap --accent without reloading the stylesheet.
const ACCENT_HEX = {
  violet: "#b78bff",
  cyan:   "#4fd9ff",
  amber:  "#ffb020",
  green:  "#22d66f",
  rose:   "#ff6da6",
};

function applyPreferences(prefs) {
  if (!prefs || typeof prefs !== "object") return;
  const root = document.documentElement;
  if (prefs.accent_color && ACCENT_HEX[prefs.accent_color]) {
    root.style.setProperty("--accent", ACCENT_HEX[prefs.accent_color]);
  }
  if (prefs.density) {
    root.dataset.density = prefs.density;
  }
  // Stash on window so other components (Clock, greeting, etc.) read live.
  window.__userPrefs = prefs;
  // Notify subscribers
  window.dispatchEvent(new CustomEvent("userprefs:changed", { detail: prefs }));
}

function ProfilePage({ currentUser }) {
  const [prefs, setPrefs] = useState(null);
  const [schema, setSchema] = useState(null);
  const [saving, setSaving] = useState(null);   // field currently being saved
  const [savedAt, setSavedAt] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    Promise.all([
      fetch("/api/user/preferences/schema", { credentials: "include" }).then(r => r.json()),
      fetch("/api/user/preferences",        { credentials: "include" }).then(r => r.json()),
    ]).then(([s, p]) => {
      setSchema(s.schema);
      setPrefs(p.preferences);
      applyPreferences(p.preferences);
    }).catch(e => setError(`Failed to load preferences: ${e}`));
  }, []);

  const updateField = async (field, value) => {
    setSaving(field); setError(null);
    try {
      const r = await fetch("/api/user/preferences", {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        credentials: "include",
        body: JSON.stringify({ [field]: value }),
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        throw new Error(j.detail || `HTTP ${r.status}`);
      }
      const j = await r.json();
      setPrefs(j.preferences);
      applyPreferences(j.preferences);
      setSavedAt(Date.now());
    } catch (e) {
      setError(`${field}: ${e.message || e}`);
    } finally {
      setSaving(null);
    }
  };

  if (error && !prefs) {
    return <div style={{ padding: 24 }}><div className="bright" style={{ color: "var(--red)" }}>{error}</div></div>;
  }
  if (!schema || !prefs) {
    return <div style={{ padding: 24, color: "var(--fg-dim)" }}>loading preferences…</div>;
  }

  const fieldOrder = [
    "display_name", "default_page", "greeting_style",
    "accent_color", "density", "time_format",
  ];

  return (
    <div style={{ padding: 24, maxWidth: 720, color: "var(--fg)" }}>
      <h2 style={{ fontSize: 18, fontWeight: 600, marginBottom: 4 }}>Profile & Preferences</h2>
      <div className="faint" style={{ marginBottom: 24, fontSize: 11 }}>
        Personal display settings. Saved per-user; takes effect immediately.
      </div>

      {/* Google identity (read-only) */}
      <div style={{
        display: "flex", alignItems: "center", gap: 14,
        padding: 14, marginBottom: 24,
        background: "var(--bg-1)", border: "1px solid var(--border)",
        borderRadius: 4,
      }}>
        {currentUser && currentUser.picture ? (
          <img src={currentUser.picture} alt="" referrerPolicy="no-referrer"
               style={{ width: 56, height: 56, borderRadius: "50%", border: "1px solid var(--border)" }}/>
        ) : (
          <div style={{
            width: 56, height: 56, borderRadius: "50%", background: "var(--bg-3)",
            display: "flex", alignItems: "center", justifyContent: "center",
            fontSize: 22, color: "var(--fg-dim)",
          }}>{(currentUser && currentUser.name || "?")[0].toUpperCase()}</div>
        )}
        <div style={{ flex: 1 }}>
          <div className="bright" style={{ fontSize: 14 }}>
            {currentUser && currentUser.name || "—"}
          </div>
          <div className="faint" style={{ fontSize: 11 }}>
            {currentUser && currentUser.email || "—"}
          </div>
        </div>
        <button onClick={_signOut} style={{
          padding: "6px 12px", background: "transparent", color: "var(--fg-dim)",
          border: "1px solid var(--border)", borderRadius: 3, cursor: "pointer",
          fontSize: 11,
        }}>SIGN OUT</button>
      </div>

      {/* Pref fields */}
      {fieldOrder.map(field => {
        const spec = schema[field];
        if (!spec) return null;
        const current = prefs[field];
        return (
          <div key={field} style={{
            display: "grid", gridTemplateColumns: "180px 1fr",
            gap: 16, padding: "14px 0", borderTop: "1px solid var(--border)",
            alignItems: "center",
          }}>
            <div>
              <div className="bright" style={{ fontSize: 12 }}>{spec.label}</div>
              <div className="faint" style={{ fontSize: 10, marginTop: 2 }}>{spec.hint}</div>
            </div>
            <div>
              {spec.type === "string" ? (
                <input
                  type="text" defaultValue={current} maxLength={spec.max_length || 200}
                  placeholder={currentUser && currentUser.name || ""}
                  onBlur={(e) => {
                    if (e.target.value !== current) updateField(field, e.target.value);
                  }}
                  style={{
                    width: "100%", padding: "6px 10px", fontSize: 12,
                    background: "var(--bg-3)", border: "1px solid var(--border)",
                    color: "var(--fg-bright)", borderRadius: 3,
                  }}
                />
              ) : spec.type === "enum" ? (
                <div style={{ display: "flex", flexWrap: "wrap", gap: 6 }}>
                  {spec.options.map(opt => {
                    const active = opt === current;
                    const showSwatch = field === "accent_color" && ACCENT_HEX[opt];
                    return (
                      <button key={opt}
                        onClick={() => updateField(field, opt)}
                        disabled={saving === field}
                        style={{
                          padding: "6px 12px", fontSize: 11,
                          background: active ? "var(--accent)" : "var(--bg-3)",
                          color: active ? "var(--accent-fg)" : "var(--fg)",
                          border: `1px solid ${active ? "var(--accent)" : "var(--border)"}`,
                          borderRadius: 3, cursor: "pointer",
                          display: "inline-flex", alignItems: "center", gap: 6,
                          textTransform: "lowercase",
                        }}>
                        {showSwatch && (
                          <span style={{
                            display: "inline-block", width: 10, height: 10, borderRadius: "50%",
                            background: ACCENT_HEX[opt], border: "1px solid var(--border-strong)",
                          }}/>
                        )}
                        {opt}
                      </button>
                    );
                  })}
                </div>
              ) : null}
            </div>
          </div>
        );
      })}

      <div style={{ marginTop: 16, fontSize: 10, color: "var(--fg-faint)" }}>
        {error ? <span style={{ color: "var(--red)" }}>error: {error}</span>
               : savedAt ? `saved ${new Date(savedAt).toLocaleTimeString("en-IN", { hour12: false })}`
               : "changes save automatically"}
      </div>
    </div>
  );
}

function App({ currentUser }) {
  const [page, setPageRaw] = useState(_initialPageFromUrl);
  const setPage = (p) => { setPageRaw(p); _syncPageToUrl(p); };
  React.useEffect(() => {
    const onPop = () => setPageRaw(_initialPageFromUrl());
    window.addEventListener("popstate", onPop);
    return () => window.removeEventListener("popstate", onPop);
  }, []);

  // Load + apply user preferences once on mount. Re-render on the resulting
  // CustomEvent so the sidebar display name picks up.
  const [userPrefs, setUserPrefs] = useState(() => window.__userPrefs || null);
  React.useEffect(() => {
    fetch("/api/user/preferences", { credentials: "include" })
      .then(r => r.ok ? r.json() : null)
      .then(j => {
        if (!j || !j.preferences) return;
        applyPreferences(j.preferences);
        setUserPrefs(j.preferences);
        // Apply default_page only when user landed on bare "/" with no
        // explicit page selected. Avoids stomping on deep links.
        try {
          const url = new URL(window.location.href);
          const hasExplicitPage = url.pathname !== "/" || url.searchParams.get("page");
          if (!hasExplicitPage && j.preferences.default_page && j.preferences.default_page !== "dashboard") {
            setPageRaw(j.preferences.default_page);
            _syncPageToUrl(j.preferences.default_page);
          }
        } catch (_e) {}
      })
      .catch(() => {});
    const onPrefs = (e) => setUserPrefs(e.detail || window.__userPrefs);
    window.addEventListener("userprefs:changed", onPrefs);
    return () => window.removeEventListener("userprefs:changed", onPrefs);
  }, []);
  const [mode, setMode] = useState("paper");
  const [quotes, setQuotes] = useState(() => store.initialQuotes());
  const [proposals, setProposals] = useState(() => store.initialProposals());
  const [bots, setBots] = useState(() => store.initialBots());
  const [positions, setPositions] = useState(() => store.initialPositions());
  const [news, setNews] = useState(() => store.initialNews());
  const [regime, setRegime] = useState("NORMAL");
  const [volScore, setVolScore] = useState(0);
  const [copilotOpen, setCopilotOpen] = useState(false);
  const [userMenuOpen, setUserMenuOpen] = useState(false);
  const [focusSymbol, setFocusSymbol] = useState(null);
  const [funds, setFunds] = useState({ opening: 0, available: 0, used: 0 });
  const [positionGroups, setPositionGroups] = useState([]);
  const [portfolioPnl,   setPortfolioPnl]   = useState({ day: 0, unrealized: 0, realized: 0, total: 0 });
  const [clientId,       setClientId]       = useState(null);
  const [market,         setMarket]         = useState({ any_open: false, nse_open: false, mcx_open: false, note: "" });
  const [vix,            setVix]            = useState(null);
  const [vixChange,      setVixChange]      = useState(null);
  const [niftyAtrPct,    setNiftyAtrPct]    = useState(null);
  const [newsSevScore,   setNewsSevScore]   = useState(null);
  const [newsEvents1h,   setNewsEvents1h]   = useState(null);
  const [backendStatus,  setBackendStatus]  = useState({ ok: false, last_fetch: null, error: "initial" });
  const [connections,    setConnections]    = useState({});

  // ======== REAL-DATA PIPE — SSE stream `/api/dashboard/stream` ========
  // Replaces the previous 5s poll of `/api/ui/state`.  Backend hub publishes
  // a `snapshot` on connect, then `dashboard_tick` events on a 3s interval
  // (and immediately on writes like proposal confirm).  No more client-side
  // setInterval; multiple browser tabs share one backend `build_full_state`.
  //
  // Keeps the SAME state-application logic the old poll used so downstream
  // components see no behavioural change.  Sparkline merge still skips ticks
  // when value is unchanged or market is closed.
  const _dashStream = useStream("/api/dashboard/stream", { bufferSize: 50 });

  const _applyDashboardState = useCallback((d) => {
    if (!d || typeof d !== "object" || Object.keys(d).length === 0) return;
    const marketOpen = (d.market || {}).any_open;
    setQuotes(prev => {
      const next = { ...prev };
      Object.entries(d.quotes || {}).forEach(([k, v]) => {
        const existing = next[k] || {};
        let mergedSpark;
        const last = v.last;
        const prevSpark = existing.sparkline || v.sparkline || [];
        const prevTick = prevSpark.length ? prevSpark[prevSpark.length - 1] : null;
        if (!prevSpark.length)                       mergedSpark = v.sparkline || [];
        else if (!marketOpen || prevTick === last)   mergedSpark = prevSpark;
        else                                         mergedSpark = [...prevSpark.slice(-39), last];
        next[k] = { ...existing, ...v, sparkline: mergedSpark };
      });
      return next;
    });
    setProposals(d.proposals || []);
    setBots(d.bots || []);
    setPositions(d.positions || []);
    setPositionGroups(d.position_groups || []);
    setPortfolioPnl(d.portfolio_pnl || { day: 0, unrealized: 0, realized: 0, total: 0 });
    setNews(d.news || []);
    setRegime(d.regime || "NORMAL");
    setVolScore(Number(d.volScore || 0));
    setFunds(d.funds || { opening: 0, available: 0, used: 0 });
    setMode(d.mode === "live" ? "live" : "paper");
    setClientId(d.client_id || null);
    setMarket(d.market || { any_open: false, note: "" });
    setVix(d.vix ?? null);
    setVixChange(d.vix_change ?? null);
    setNiftyAtrPct(d.nifty_atr_pct ?? null);
    setNewsSevScore(d.news_severity_score ?? null);
    setNewsEvents1h(d.news_events_1h ?? null);
    setConnections(d.connections || {});
  }, []);

  // Apply each new snapshot frame (initial + reconnect bootstrap).
  useEffect(() => { _applyDashboardState(_dashStream.snapshot); },
            [_dashStream.snapshot, _applyDashboardState]);

  // Apply each periodic `dashboard_tick` (and surface vendor lifecycle on
  // proposal_changed events — the tick that follows will reflect the write).
  useEffect(() => {
    const ev = _dashStream.lastEvent;
    if (!ev) return;
    if (ev.type === "dashboard_tick" && ev.data) _applyDashboardState(ev.data);
  }, [_dashStream.lastEvent, _applyDashboardState]);

  // Mirror connection status onto the existing backendStatus object so the
  // existing topbar widgets keep working.
  useEffect(() => {
    if (_dashStream.status === "open") {
      setBackendStatus({ ok: true, last_fetch: new Date().toISOString(), error: null });
    } else if (_dashStream.error) {
      setBackendStatus({ ok: false, last_fetch: new Date().toISOString(),
                         error: _dashStream.error });
    }
  }, [_dashStream.status, _dashStream.error]);

  // ---- Actions wired to real backend ----
  const confirmProposal = useCallback(async (id) => {
    setProposals(prev => prev.map(p => p.request_id === id ? { ...p, status: "confirmed" } : p));
    try {
      await fetch("/api/proposals/" + id + "/confirm", { method: "POST" });
    } catch (e) { /* next poll will correct */ }
  }, []);

  const rejectProposal = useCallback(async (id) => {
    setProposals(prev => prev.map(p => p.request_id === id ? { ...p, status: "cancelled" } : p));
    try {
      await fetch("/api/ui/proposals/" + id + "/reject", { method: "POST" });
    } catch (e) {}
  }, []);

  const toggleBot = useCallback(async (id) => {
    const bot = bots.find(b => b.bot_id === id);
    if (!bot) return;
    const action = bot.enabled ? "disable" : "enable";
    setBots(prev => prev.map(b => b.bot_id === id ? { ...b, enabled: !b.enabled } : b));
    try { await fetch("/api/bots/" + id + "/" + action, { method: "POST" }); } catch (e) {}
  }, [bots]);

  const runBotNow = useCallback(async (id) => {
    try { await fetch("/api/bots/" + id + "/run_now", { method: "POST" }); } catch (e) {}
  }, []);

  const addBot = useCallback(async (b) => {
    try {
      await fetch("/api/bots", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ ...b, enabled: !!b.enabled }),
      });
    } catch (e) {}
  }, []);

  const updateBot = useCallback(async (id, changes) => {
    const existing = bots.find(b => b.bot_id === id);
    if (!existing) return;
    try {
      await fetch("/api/bots/" + id, {
        method: "PUT",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ ...existing, ...changes }),
      });
    } catch (e) {}
  }, [bots]);

  const deleteBot = useCallback(async (id) => {
    try { await fetch("/api/bots/" + id, { method: "DELETE" }); } catch (e) {}
  }, []);

  const pushNews = async ({ headline, severity, category, source = "Manual", body, symbols }) => {
    try {
      await fetch("/api/news", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          headline, body: body || "", source,
          symbols_affected: symbols || [],
          severity_override: severity,
          category_override: category,
        }),
      });
    } catch (e) {}
  };

  const injectCritical = async () => {
    await pushNews({
      headline: "⚠ BREAKING: Geopolitical escalation — major military action reported near critical energy corridor",
      severity: "CRITICAL", category: "WAR_GEOPOLITICS", source: "Simulation",
      body: "Reports of significant escalation. Crude futures spiking. Expect high volatility across Indian equities, especially energy and financials.",
      symbols: ["NIFTY", "BANKNIFTY", "RELIANCE", "ONGC"],
    });
  };

  const resetRegime = () => { /* regime recalculates from news decay on next poll */ };

  const app = {
    mode, setMode, regime, volScore, funds,
    proposals, confirmProposal, rejectProposal,
    bots, toggleBot, runBotNow, addBot, updateBot, deleteBot,
    positions, position_groups: positionGroups, portfolio_pnl: portfolioPnl,
    news, pushNews, resetRegime, clientId, backendStatus,
    vix, vix_change: vixChange,
    nifty_atr_pct: niftyAtrPct,
    news_severity_score: newsSevScore,
    news_events_1h: newsEvents1h,
    regimeInfo: {
      rec: regime === "EXTREME" ? "Halt new entries. Tighten stops on open positions. Reduce size ≥ 50%."
         : regime === "HIGH"    ? "Avoid fresh longs. Take partial profits on trend followers."
         : regime === "ELEVATED"? "Reduce position size to 50%. Prefer defined-risk entries."
         : regime === "WATCH"   ? "Proceed cautiously. Validate with multiple timeframes."
         : "Normal conditions. Standard position sizing and rules apply.",
    },
  };

  const pendingCount = proposals.filter(p => p.status === "pending" && p.risk_passed).length;

  const pages = {
    dashboard: <DashboardPage app={app} quotes={quotes} onGoto={setPage} onInjectCritical={injectCritical} onFocusSymbol={(s) => { setFocusSymbol(s); setPage("scanner_symbols"); }} />,
    scanner:          <ScannerPage app={app} quotes={quotes} onFocusSymbol={(s) => { setFocusSymbol(s); setPage("scanner_symbols"); }}/>,
    scanner_symbols:  <SymbolsPage app={app} quotes={quotes} focusSymbol={focusSymbol} onFocusSymbol={setFocusSymbol}/>,
    options:   <OptionsPage app={app} quotes={quotes}/>,
    backtest:  <BacktestPage app={app}/>,
    news:      <NewsPage app={app}/>,
    bots:      <BotsPage app={app}/>,
    council:            <PortfolioCouncilPage app={app} quotes={quotes}/>,
    council_swing:      <CouncilPage app={app} quotes={quotes} initialView="live" councilType="swing"/>,
    council_trade:      <CouncilPage app={app} quotes={quotes} initialView="live" councilType="trade"/>,
    council_investment: <CouncilPage app={app} quotes={quotes} initialView="live" councilType="investment"/>,
    council_proposals:  <ProposalsPage app={app}/>,
    council_history:    <CouncilPage app={app} quotes={quotes} initialView="history" councilType="swing"/>,
    arbitrage: <ArbitragePage app={app}/>,
    tick:      <TickPage app={app}/>,
    system:    <SystemPage app={app} currentUser={currentUser}/>,
    sectors:   <SectorsPage app={app}/>,
    global:    <GlobalPage app={app}/>,
    calendars: <CalendarsPage app={app}/>,
    apikeys:   <ApiKeysPage app={app}/>,
    profile:   <ProfilePage currentUser={currentUser}/>,
    settings:  <SettingsPage app={app}/>,
  };

  const navItems = [
    { k: "dashboard", l: "Dashboard", icon: "◈" },
    { k: "scanner",   l: "Scanner",   icon: "▦", subItems: [
        { k: "scanner_symbols", l: "Symbols", icon: "◉" },
      ] },
    { k: "options",   l: "Options",   icon: "⊙" },
    { k: "backtest",  l: "Backtest",  icon: "↺" },
    { k: "news",      l: "News",      icon: "▸", badge: news.filter(n => ["HIGH", "CRITICAL"].includes(n.severity) && (Date.now() - new Date(n.received_at).getTime()) < 3600000).length },
    { k: "bots",      l: "Bots",      icon: "⚙", badge: bots.filter(b => b.enabled).length },
    { k: "council",   l: "Council",   icon: "✦", badge: pendingCount, subItems: [
        { k: "council_proposals",  l: "Proposals",  icon: "◆", badge: pendingCount },
        { k: "council_swing",      l: "Swing",      icon: "✦" },
        { k: "council_trade",      l: "Trade",      icon: "◐" },
        { k: "council_investment", l: "Investment", icon: "◈" },
        { k: "council_history",    l: "History",    icon: "↺" },
      ] },
    { k: "arbitrage", l: "Arbitrage", icon: "⇄" },
    { k: "tick",      l: "Tick",      icon: "◎" },
    ...(currentUser && currentUser.email && ["rajatgupta.devices@gmail.com"].includes(String(currentUser.email).toLowerCase())
        ? [{ k: "system", l: "System", icon: "⚡" }]
        : []),
    { k: "sectors",   l: "Sectors",   icon: "▥" },
    { k: "global",    l: "Global",    icon: "⊕" },
    { k: "calendars", l: "Calendars", icon: "▤" },
  ];

  return (
    <div className="app-root">
      {/* TOP BAR */}
      <header className="topbar">
        <div className="brand" role="button" tabIndex={0}
             onClick={() => setPage("dashboard")}
             onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") setPage("dashboard"); }}
             title="Go to dashboard"
             style={{ display: "flex", alignItems: "center", gap: 8, cursor: "pointer" }}>
          <BrandMark/>
          <span className="amber" style={{ fontSize: 16, fontWeight: 700, letterSpacing: "0.18em" }}>TULAKER</span>
        </div>
        <div className="sep"/>
        <div className="topbar-stat">
          <span className="h-xxs">MODE</span>
          <span className={`pill ${mode === "live" ? "pill-red-solid" : "pill-amber"}`} style={{ fontWeight: 600, letterSpacing: "0.1em" }}>
            {mode === "live" ? "● LIVE" : "◐ PAPER"}
          </span>
        </div>
        <div className="topbar-stat">
          <span className="h-xxs">REGIME</span>
          <span className={`pill ${regime === "NORMAL" ? "pill-up" : regime === "WATCH" ? "pill-cyan" : regime === "ELEVATED" ? "pill-amber" : regime === "HIGH" ? "pill-down" : "pill-red-solid"}`}>
            {regime}
          </span>
        </div>
        <div className="topbar-stat">
          <span className="h-xxs">BACKEND</span>
          <span className={`pill ${backendStatus.ok ? "pill-up" : "pill-red-solid"}`}>
            <span className={`dot ${backendStatus.ok ? "dot-live" : "dot-critical"}`} style={{ width: 5, height: 5 }}/>
            {backendStatus.ok ? "LIVE" : "OFFLINE"}
          </span>
        </div>
        <div className="topbar-stat" title={market.note || ""}>
          <span className="h-xxs">MARKET</span>
          <span className={`pill ${market.any_open ? "pill-up" : "pill-dim"}`}>
            <span className={`dot ${market.any_open ? "dot-live" : "dot-dim"}`} style={{ width: 5, height: 5 }}/>
            {market.nse_open ? "NSE OPEN" : market.mcx_open ? "MCX OPEN" : "CLOSED"}
          </span>
        </div>
        <ConnectionPill label="DHAN" conn={connections.dhan}/>
        <ConnectionPill label="TV"   conn={connections.tradingview}/>
        <ConnectionPill label="NSE"  conn={connections.nse_feed}/>
        <ConnectionPill label="MCX"  conn={connections.mcx_feed}/>
        <ConnectionPill label="NEWS" conn={connections.news_feed}/>
        <div style={{ flex: 1 }}/>
        <div className="topbar-stat" title={market.any_open ? "today's realized + MTM change" : "market closed — day P&L frozen at close"}>
          <span className="h-xxs">DAY P&L {market.any_open ? "" : "(closed)"}</span>
          <span className={`tnum ${
            !backendStatus.ok ? "faint" :
            !market.any_open  ? "faint" :
            ((portfolioPnl.day || 0) >= 0 ? "up" : "down")
          }`} style={{ fontSize: 13, fontWeight: 500 }}>
            {backendStatus.ok ? store.inrSigned(portfolioPnl.day || 0) : "—"}
          </span>
        </div>
        <div className="topbar-stat">
          <span className="h-xxs">TOTAL P&L</span>
          <span className={`tnum ${
            !backendStatus.ok ? "faint" :
            ((portfolioPnl.total || 0) >= 0 ? "up" : "down")
          }`} style={{ fontSize: 13, fontWeight: 500 }}>
            {backendStatus.ok ? store.inrSigned(portfolioPnl.total || 0) : "—"}
          </span>
        </div>
        <div className="topbar-stat">
          <span className="h-xxs">AVAIL</span>
          <span className={`tnum ${backendStatus.ok ? "bright" : "faint"}`} style={{ fontSize: 13 }}>
            {backendStatus.ok ? store.inr(funds.available) : "—"}
          </span>
        </div>
        <div className="sep"/>
        <button className={`btn ${copilotOpen ? "btn-primary" : ""}`} onClick={() => setCopilotOpen(v => !v)}>
          <span className="violet">◆</span> CLAUDE
          {pendingCount ? <span className="panel-tag" style={{ marginLeft: 6 }}>{pendingCount}</span> : null}
        </button>
        <div className="topbar-stat" style={{ paddingRight: 6 }}>
          <Clock/>
        </div>
      </header>

      {/* BODY */}
      <div className="body">
        <nav className="sidenav">
          {navItems.map(item => {
            return (
              <React.Fragment key={item.k}>
                <button className={`navbtn ${page === item.k ? "active" : ""}`} onClick={() => setPage(item.k)}>
                  <span className="navicon">{item.icon}</span>
                  <span className="navlabel">{item.l}</span>
                  {item.badge ? <span className="navbadge">{item.badge}</span> : null}
                </button>
                {item.subItems ? item.subItems.map(sub => (
                  <button key={sub.k} className={`navbtn nav-sub ${page === sub.k ? "active" : ""}`} onClick={() => setPage(sub.k)}>
                    <span className="navicon">{sub.icon || "·"}</span>
                    <span className="navlabel">{sub.l}</span>
                    {sub.badge ? <span className="navbadge">{sub.badge}</span> : null}
                  </button>
                )) : null}
              </React.Fragment>
            );
          })}
          <div style={{ flex: 1 }}/>
          <div style={{ padding: 8, borderTop: "1px solid var(--border)", fontSize: 9, position: "relative" }}>
            <div className="h-xxs" style={{ marginBottom: 6 }}>USER</div>
            {currentUser ? (
              <button onClick={() => setUserMenuOpen(v => !v)}
                      title="Open user menu"
                      style={{
                        display: "flex", alignItems: "center", gap: 8, width: "100%",
                        padding: 4,
                        background: userMenuOpen ? "var(--bg-3)" : "transparent",
                        border: `1px solid ${userMenuOpen ? "var(--border)" : "transparent"}`,
                        borderRadius: 3, cursor: "pointer", textAlign: "left",
                      }}
                      onMouseEnter={e => { if (!userMenuOpen) e.currentTarget.style.borderColor = "var(--border)"; }}
                      onMouseLeave={e => { if (!userMenuOpen) e.currentTarget.style.borderColor = "transparent"; }}>
                {currentUser.picture ? (
                  <img src={currentUser.picture} alt="" referrerPolicy="no-referrer"
                       style={{ width: 22, height: 22, borderRadius: "50%", flexShrink: 0 }}/>
                ) : (
                  <span style={{
                    width: 22, height: 22, borderRadius: "50%", flexShrink: 0,
                    background: "var(--bg-3)", display: "inline-flex",
                    alignItems: "center", justifyContent: "center", fontSize: 10,
                  }}>{(currentUser.name || currentUser.email || "?")[0].toUpperCase()}</span>
                )}
                <span className="bright" style={{
                  overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
                  fontSize: 10, flex: 1,
                }}>
                  {(userPrefs && userPrefs.display_name) || currentUser.name || currentUser.email}
                </span>
                <span className="faint" style={{ fontSize: 9 }}>{userMenuOpen ? "▾" : "▴"}</span>
              </button>
            ) : (
              <div className="bright">{backendStatus.ok ? "trader@dhan" : "—"}</div>
            )}
            <div className="faint" style={{ marginTop: 4 }}>{clientId ? "ID " + clientId : "—"}</div>

            {/* User menu — pops upward above the button */}
            {currentUser && userMenuOpen && (
              <div style={{
                position: "absolute", bottom: "100%", left: 6, right: 6,
                marginBottom: 4, padding: 4,
                background: "var(--bg-2)", border: "1px solid var(--border-strong)",
                borderRadius: 4, boxShadow: "0 -4px 12px rgba(0,0,0,0.4)",
                display: "flex", flexDirection: "column", gap: 2,
                zIndex: 100,
              }}>
                {[
                  { k: "profile",  l: "Profile & Preferences" },
                  { k: "apikeys",  l: "API Keys" },
                  { k: "settings", l: "App Settings" },
                ].map(item => (
                  <button key={item.k}
                          onClick={() => { setPage(item.k); setUserMenuOpen(false); }}
                          style={{
                            padding: "6px 10px", fontSize: 11,
                            background: page === item.k ? "var(--bg-3)" : "transparent",
                            color: page === item.k ? "var(--fg-bright)" : "var(--fg)",
                            border: "1px solid transparent", borderRadius: 3,
                            cursor: "pointer", textAlign: "left",
                          }}
                          onMouseEnter={e => e.currentTarget.style.background = "var(--bg-hover)"}
                          onMouseLeave={e => e.currentTarget.style.background = page === item.k ? "var(--bg-3)" : "transparent"}>
                    {item.l}
                  </button>
                ))}
                <div style={{ height: 1, background: "var(--border)", margin: "2px 0" }}/>
                <button onClick={() => { setUserMenuOpen(false); _signOut(); }}
                        style={{
                          padding: "6px 10px", fontSize: 11,
                          background: "transparent", color: "var(--red)",
                          border: "1px solid transparent", borderRadius: 3,
                          cursor: "pointer", textAlign: "left",
                        }}
                        onMouseEnter={e => e.currentTarget.style.background = "var(--bg-hover)"}
                        onMouseLeave={e => e.currentTarget.style.background = "transparent"}>
                  Sign out
                </button>
              </div>
            )}
          </div>
        </nav>

        <main className="content" style={{
          position: "relative",
          filter: backendStatus.ok ? "none" : "grayscale(0.7) opacity(0.5)",
          pointerEvents: backendStatus.ok ? "auto" : "none",
          transition: "filter 200ms ease",
        }}>
          {pages[page]}
          {!backendStatus.ok ? (
            <div style={{
              position: "absolute", top: 12, left: "50%", transform: "translateX(-50%)",
              background: "var(--bg-1)", border: "1px solid var(--critical)",
              color: "var(--critical)", padding: "6px 14px",
              fontSize: 11, letterSpacing: "0.1em", zIndex: 10, pointerEvents: "auto",
            }}>
              ◉ BACKEND OFFLINE — retrying every 5s · {backendStatus.error || "no response"}
            </div>
          ) : null}
        </main>
      </div>

      {/* TICKER */}
      <TickerTape quotes={quotes}/>

      {/* CLAUDE DRAWER */}
      <ClaudeDrawer open={copilotOpen} onClose={() => setCopilotOpen(false)} app={app} quotes={quotes} onGoto={setPage}/>
    </div>
  );
}

// ===== AUTH GATE =====
// Fetches /api/auth/me on mount. If 401 → render LoginPage; if 200 → render App.
// Surfaces login_error from the redirect query string when OAuth was denied.
function LoginPage({ error }) {
  const errorMessages = {
    oauth_failed:       "Google sign-in failed. Try again.",
    no_email:           "Google didn't return an email address. Try again.",
    email_not_allowed:  "This Google account is not authorized for this app.",
  };
  const msg = error && (errorMessages[error] || `Login error: ${error}`);
  return (
    <div style={{
      display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center",
      height: "100vh", background: "#0a0d12", color: "#e6edf3", fontFamily: "monospace",
    }}>
      <img src="/terminal/tulaker-logo.png" alt="Tulaker" width="120" height="120"
           style={{ marginBottom: 16, mixBlendMode: "lighten", objectFit: "contain" }}/>
      <div style={{ fontSize: 28, fontWeight: 700, letterSpacing: "0.22em", color: "#ffb020", marginBottom: 6 }}>TULAKER</div>
      <div style={{ fontSize: 11, opacity: 0.5, letterSpacing: "0.18em", marginBottom: 32 }}>TRADING TERMINAL · AUTHENTICATED ACCESS REQUIRED</div>
      {msg && (
        <div style={{
          padding: "10px 16px", marginBottom: 24, border: "1px solid #d33",
          background: "#2a0a0a", color: "#f88", borderRadius: 4, fontSize: 13,
        }}>{msg}</div>
      )}
      <a href="/auth/google/login" style={{
        display: "inline-flex", alignItems: "center", gap: 10,
        padding: "12px 24px", background: "#fff", color: "#1a1a1a",
        borderRadius: 4, textDecoration: "none", fontSize: 14, fontWeight: 500,
      }}>
        <svg width="18" height="18" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
          <path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" fill="#4285F4"/>
          <path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84A10.99 10.99 0 0 0 12 23z" fill="#34A853"/>
          <path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18A10.99 10.99 0 0 0 1 12c0 1.77.42 3.45 1.18 4.93l3.66-2.84z" fill="#FBBC05"/>
          <path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1A10.99 10.99 0 0 0 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" fill="#EA4335"/>
        </svg>
        Sign in with Google
      </a>
      <div style={{ marginTop: 24, fontSize: 11, opacity: 0.4 }}>
        Only authorised accounts can sign in.
      </div>
    </div>
  );
}

function AuthGate() {
  const [state, setState] = useState({ loading: true, user: null });
  useEffect(() => {
    fetch("/api/auth/me", { credentials: "include" })
      .then(r => r.status === 200 ? r.json() : Promise.reject(r.status))
      .then(d => {
        // Drop a stale ?login_error=... from the URL when the user is in fact
        // authenticated (an older session cookie carried them through after a
        // failed re-login). Leaving the param in the address bar is misleading.
        const url = new URL(window.location.href);
        if (url.searchParams.has("login_error")) {
          url.searchParams.delete("login_error");
          window.history.replaceState({}, "", url.pathname + url.search + url.hash);
        }
        setState({ loading: false, user: d.user });
      })
      .catch(() => setState({ loading: false, user: null }));
  }, []);

  if (state.loading) {
    return (
      <div style={{
        display: "flex", alignItems: "center", justifyContent: "center",
        height: "100vh", background: "#0a0d12", color: "#888", fontFamily: "monospace",
      }}>checking session…</div>
    );
  }
  if (!state.user) {
    const params = new URLSearchParams(window.location.search);
    return <LoginPage error={params.get("login_error")}/>;
  }
  return <App currentUser={state.user}/>;
}

ReactDOM.createRoot(document.getElementById("root")).render(<AuthGate/>);
