/* app.jsx — top-level shell, tabs, live data, Tweaks panel.
   Fetches servers/channels/analysts from the API on mount. */

const TABS = [
  { k: "positions", label: "Positions", title: "Open Positions",     icon: "table" },
  { k: "events",    label: "Events",    title: "Signal Events",      icon: "events" },
  { k: "sources",   label: "Sources",   title: "Sources",            icon: "sources" },
  { k: "charts",    label: "Charts",    title: "Charts",             icon: "chart" },
  { k: "watchlist", label: "Watchlist", title: "Watchlist",          icon: "eye" },
  { k: "recaps",    label: "Recaps",    title: "Historical Recaps",  icon: "recap" },
  { k: "settings",  label: "Settings",  title: "Settings",           icon: "settings" },
];

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "density": "default",
  "showParser": true
}/*EDITMODE-END*/;

// Theme: "light" | "dark" | "system" — persisted to localStorage.
// "system" follows the OS preference and updates live if it changes.
const THEME_KEY = "tw:theme";
function getStoredTheme() {
  try { return localStorage.getItem(THEME_KEY) || "system"; } catch { return "system"; }
}
function useTheme() {
  const [mode, setMode] = useState(getStoredTheme);
  useEffect(() => {
    try { localStorage.setItem(THEME_KEY, mode); } catch {}
    const apply = (resolved) => document.documentElement.setAttribute("data-theme", resolved);
    if (mode === "system") {
      const mq = window.matchMedia("(prefers-color-scheme: dark)");
      const onChange = () => apply(mq.matches ? "dark" : "light");
      onChange();
      mq.addEventListener("change", onChange);
      return () => mq.removeEventListener("change", onChange);
    }
    apply(mode);
  }, [mode]);
  return [mode, setMode];
}

function ServerMultiSelect({ servers, selected, onChange }) {
  const [open, setOpen] = useState(false);
  const ref = useRef(null);

  useEffect(() => {
    if (!open) return;
    const onClick = (e) => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); };
    window.addEventListener("mousedown", onClick);
    return () => window.removeEventListener("mousedown", onClick);
  }, [open]);

  function toggle(id) {
    const next = new Set(selected);
    if (next.has(id)) next.delete(id); else next.add(id);
    onChange(next);
  }
  function selectAll() { onChange(new Set(servers.map(s => s.guild_id))); }
  function selectNone() { onChange(new Set()); }

  const label = (() => {
    if (selected.size === 0) return "No servers";
    if (selected.size === servers.length) return `All ${servers.length} servers`;
    if (selected.size === 1) {
      const id = [...selected][0];
      return servers.find(s => s.guild_id === id)?.name || "1 server";
    }
    return `${selected.size} of ${servers.length} servers`;
  })();

  return (
    <div ref={ref} style={{ position: "relative" }}>
      <button
        onClick={() => setOpen(o => !o)}
        className="btn"
        style={{ padding: "8px 12px", fontFamily: "var(--f-mono)", fontSize: 12, display: "inline-flex", alignItems: "center", gap: 8 }}
      >
        {I("database", { size: 14 })}
        {label}
        {I("chevDown", { size: 12 })}
      </button>
      {open && (
        <div style={{
          position: "absolute", top: "calc(100% + 4px)", right: 0, zIndex: 50,
          minWidth: 260,
          background: "var(--bg-1)",
          border: "1px solid var(--border-2)",
          borderRadius: "var(--radius)",
          boxShadow: "var(--shadow-2)",
          overflow: "hidden",
        }}>
          <div style={{
            display: "flex", justifyContent: "space-between",
            padding: "8px 10px", borderBottom: "1px solid var(--border-1)",
            fontFamily: "var(--f-mono)", fontSize: 10, letterSpacing: ".10em",
            textTransform: "uppercase", color: "var(--fg-2)",
          }}>
            <span>Servers</span>
            <span style={{ display: "flex", gap: 8 }}>
              <button onClick={selectAll} style={{ color: "var(--accent)" }}>all</button>
              <button onClick={selectNone} style={{ color: "var(--fg-2)" }}>none</button>
            </span>
          </div>
          <div style={{ maxHeight: 320, overflowY: "auto" }}>
            {servers.length === 0 && (
              <div style={{ padding: 16, fontSize: 12, color: "var(--fg-2)" }}>No servers registered.</div>
            )}
            {servers.map(s => {
              const on = selected.has(s.guild_id);
              return (
                <label key={s.guild_id} style={{
                  display: "flex", alignItems: "center", gap: 10,
                  padding: "10px 12px",
                  borderBottom: "1px solid var(--border-1)",
                  cursor: "pointer",
                  background: on ? "var(--accent-glow)" : "transparent",
                }}>
                  <input type="checkbox" checked={on} onChange={() => toggle(s.guild_id)} />
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontSize: 13, color: "var(--fg-0)" }}>{s.name}</div>
                    <div className="mono" style={{ fontSize: 10, color: "var(--fg-3)" }}>{s.guild_id}</div>
                  </div>
                </label>
              );
            })}
          </div>
        </div>
      )}
    </div>
  );
}

function ThemeSwitch({ mode, onChange }) {
  const opts = [
    { v: "light",  icon: "sun",     title: "Light" },
    { v: "dark",   icon: "moon",    title: "Dark" },
    { v: "system", icon: "monitor", title: "System" },
  ];
  return (
    <div style={{
      display: "inline-flex",
      border: "1px solid var(--border-1)",
      borderRadius: "var(--radius)",
      overflow: "hidden",
      background: "var(--bg-1)",
    }}>
      {opts.map(o => (
        <button
          key={o.v}
          title={o.title}
          onClick={() => onChange(o.v)}
          style={{
            padding: "8px 10px",
            background: mode === o.v ? "var(--accent-glow)" : "transparent",
            color: mode === o.v ? "var(--accent)" : "var(--fg-2)",
            borderRight: o.v !== "system" ? "1px solid var(--border-1)" : "none",
            display: "flex", alignItems: "center",
          }}
        >
          {I(o.icon, { size: 14 })}
        </button>
      ))}
    </div>
  );
}

// API base — same-origin in prod, localhost:3001 in dev
function apiBase() {
  if (typeof window === "undefined") return "";
  const { hostname, origin } = window.location;
  if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "") {
    return "http://localhost:3001";
  }
  return origin;
}

// Normalize a DB analyst row into the shape the UI components expect
function normalizeAnalyst(row) {
  return {
    handle: row.handle,
    color: row.color || "var(--a-1)",
    priority: row.priority || "core",
    channels: row.channel_scope || [],   // UI reads .channels
    events: 0,                            // populated by computing from events
    discord_user_id: row.discord_user_id,
    is_bot: row.is_bot,
    enabled: row.enabled !== false,
    id: row.id,
  };
}

// Normalize a DB channel row into the shape ChannelsScreen expects
function normalizeChannel(row) {
  return {
    id: row.channel_id,
    name: row.name,
    category: row.category,
    instrument_mode: row.instrument_mode || "any",
    msgs: 0,
    owner_user_id: row.owner_user_id,
    enabled: row.enabled !== false,
  };
}

class ErrorBoundary extends React.Component {
  constructor(props) { super(props); this.state = { error: null }; }
  static getDerivedStateFromError(error) { return { error }; }
  componentDidCatch(err, info) { console.error("[sinux-signals] render error:", err, info); }
  render() {
    if (this.state.error) {
      return (
        <div style={{
          padding: 24, margin: 14, border: "1px solid var(--border-2)",
          background: "var(--bg-2)", borderRadius: "var(--radius)",
          fontFamily: "var(--f-mono)", fontSize: 12, color: "var(--fg-1)",
        }}>
          <div style={{ color: "var(--c-down)", marginBottom: 10, fontSize: 14 }}>
            {I("alert", { size: 14 })} This view crashed.
          </div>
          <pre style={{ whiteSpace: "pre-wrap", color: "var(--fg-2)", margin: 0 }}>
            {String(this.state.error?.stack || this.state.error?.message || this.state.error)}
          </pre>
          <button className="btn" style={{ marginTop: 12 }} onClick={() => this.setState({ error: null })}>
            Reset
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// Discord profile avatar (image, or initial fallback)
function Avatar({ user, size = 26 }) {
  const url = user.avatar ? `https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png?size=64` : null;
  const name = user.global_name || user.username || "?";
  if (url) return <img src={url} alt="" width={size} height={size} style={{ borderRadius: "50%", flexShrink: 0 }} />;
  return (
    <span style={{ width: size, height: size, borderRadius: "50%", background: "var(--accent)", color: "var(--bg-0)", display: "grid", placeItems: "center", fontSize: 11, fontWeight: 600, flexShrink: 0 }}>
      {name[0].toUpperCase()}
    </span>
  );
}

function AuthSplash() {
  return (
    <div className="login-gate">
      <div className="login-card">
        <div className="brand"><span className="mark">S</span><span className="brand-name">Sinux Signals</span></div>
        <p style={{ color: "var(--fg-2)" }}>Loading…</p>
      </div>
    </div>
  );
}

function LoginGate() {
  return (
    <div className="login-gate">
      <div className="login-card">
        <div className="brand"><span className="mark">S</span><span className="brand-name">Sinux Signals</span></div>
        <p>Discord trade-signal desk. Sign in to continue.</p>
        <a className="btn primary discord-btn" href={apiBase() + "/api/auth/login"}>
          <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
            <path d="M20 4.4A19 19 0 0 0 15.2 3l-.3.5a17 17 0 0 1 4.2 1.3 16 16 0 0 0-12.2 0A17 17 0 0 1 11.1 3.5L10.8 3A19 19 0 0 0 6 4.4C2.9 9 2 13.5 2.4 18a19 19 0 0 0 5.8 2.9l.6-.9a12 12 0 0 1-1.9-.9l.5-.4a13 13 0 0 0 11.2 0l.5.4a12 12 0 0 1-1.9.9l.6.9A19 19 0 0 0 21.6 18c.5-5.2-.8-9.7-3.6-13.6zM9.3 15.3c-1.1 0-2-1-2-2.3s.9-2.3 2-2.3 2.1 1 2 2.3c0 1.3-.9 2.3-2 2.3zm5.4 0c-1.1 0-2-1-2-2.3s.9-2.3 2-2.3 2.1 1 2 2.3c0 1.3-.9 2.3-2 2.3z" />
          </svg>
          Sign in with Discord
        </a>
      </div>
    </div>
  );
}

function AccessDenied({ reason }) {
  async function differentAccount() {
    try { await tapeFetch(apiBase() + "/api/auth/logout", { method: "POST" }); } catch (_) {}
    window.location.href = apiBase() + "/api/auth/login";
  }
  return (
    <div className="login-gate">
      <div className="login-card denied-card">
        <div className="brand"><span className="mark">S</span><span className="brand-name">Sinux Signals</span></div>
        <div className="denied-badge">{I("lock", { size: 24 })}</div>
        <h2 className="denied-title">Access denied</h2>
        <p className="denied-reason">{reason || "Your Discord account doesn't have access to Sinux Signals."}</p>
        <p className="denied-sub">If you think this is a mistake, ask an admin to grant your role or add your account under Settings → Access.</p>
        <button className="btn discord-btn" onClick={differentAccount}>
          {I("logout", { size: 16 })} Try a different account
        </button>
      </div>
    </div>
  );
}

function App() {
  const [tab, setTab] = useState("positions");
  const [navOpen, setNavOpen] = useState(false);   // mobile nav drawer
  const [filter, setFilter] = useState("");
  const [days, setDays] = useState(14);
  const [minTrades, setMinTrades] = useState(1);
  const [currentTicker, setCurrentTicker] = useState(null);
  const [flyoutEvent, setFlyoutEvent] = useState(null);
  const [liveCfg, setLiveCfgState] = useState(() => getLiveConfig());

  // Auth: { checked, enabled (is Discord login configured server-side), user, denied, reason }
  const [auth, setAuth] = useState({ checked: false, enabled: false, user: null, denied: false, reason: "" });
  useEffect(() => {
    // A denied login bounces back here with ?denied=1&reason=… (no session set).
    const params = new URLSearchParams(window.location.search);
    const deniedParam = params.get("denied") === "1";
    const reasonParam = params.get("reason") || "";
    if (deniedParam) window.history.replaceState({}, "", window.location.pathname);
    tapeFetch(apiBase() + "/api/auth/me")
      .then(async r => {
        const j = await r.json().catch(() => ({}));
        // 403 = signed in but not permitted (e.g. role got blocked) → denied wall.
        const denied = deniedParam || r.status === 403;
        setAuth({ checked: true, enabled: !!j.enabled || deniedParam, user: j.user || null, denied, reason: reasonParam });
      })
      .catch(() => setAuth({ checked: true, enabled: deniedParam, user: null, denied: deniedParam, reason: reasonParam }));
  }, []);
  function logout() {
    tapeFetch(apiBase() + "/api/auth/logout", { method: "POST" }).finally(() => window.location.reload());
  }

  // Real data from the API
  const [servers, setServers] = useState([]);
  const [selectedServerIds, setSelectedServerIds] = useState(new Set());
  const [analysts, setAnalysts] = useState([]);
  const [channels, setChannels] = useState([]);
  const [enabled, setEnabled] = useState(new Set());
  const [watchlist, setWatchlist] = useState([]);

  // Stable comma-separated key for use in fetch params + effect deps
  const serverIdsParam = useMemo(
    () => [...selectedServerIds].sort().join(","),
    [selectedServerIds]
  );

  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [themeMode, setThemeMode] = useTheme();

  // Listen for Settings → save cfg events
  useEffect(() => {
    const handler = () => setLiveCfgState(getLiveConfig());
    window.addEventListener("tw:liveconfig", handler);
    return () => window.removeEventListener("tw:liveconfig", handler);
  }, []);

  // Fetch servers once auth is resolved. Fetching on bare mount can race ahead
  // of the session being ready and return nothing (one-shot, never retried) —
  // which left all-servers users stuck on "No servers" while signals still loaded.
  useEffect(() => {
    if (!auth.checked) return;
    if (auth.enabled && !auth.user) return;   // signed out → nothing to load
    tapeFetch(apiBase() + "/api/servers")
      .then(r => r.json())
      .then(({ servers }) => setServers(servers || []))
      .catch(err => console.error("[sinux-signals] servers fetch failed:", err));
  }, [auth.checked, auth.enabled, auth.user?.id]);

  // Default the selection to every visible server once the list loads — but only
  // while nothing is selected, so a manual choice is preserved.
  useEffect(() => {
    if (servers.length && selectedServerIds.size === 0) {
      setSelectedServerIds(new Set(servers.map(s => s.guild_id)));
    }
  }, [servers]);

  // Fetch channels + analysts whenever the selected servers change
  useEffect(() => {
    if (!serverIdsParam) return;
    tapeFetch(apiBase() + "/api/channels?server_ids=" + serverIdsParam)
      .then(r => r.json())
      .then(({ channels }) => setChannels((channels || []).map(normalizeChannel)))
      .catch(err => console.error("[sinux-signals] channels fetch failed:", err));
    tapeFetch(apiBase() + "/api/analysts?server_ids=" + serverIdsParam)
      .then(r => r.json())
      .then(({ analysts }) => {
        const norm = (analysts || []).map(normalizeAnalyst);
        setAnalysts(norm);
        setEnabled(new Set(norm.filter(a => a.enabled).map(a => a.handle)));
      })
      .catch(err => console.error("[sinux-signals] analysts fetch failed:", err));
  }, [serverIdsParam]);

  // Watchlist — poll on an interval so new entries appear without a refresh
  // (the events feed has its own live source; watchlist piggybacks on a timer).
  useEffect(() => {
    if (!serverIdsParam) return;
    let alive = true;
    const load = () => tapeFetch(apiBase() + "/api/watchlist?server_ids=" + serverIdsParam)
      .then(r => r.json())
      .then(({ entries }) => { if (alive) setWatchlist(entries || []); })
      .catch(err => console.error("[sinux-signals] watchlist fetch failed:", err));
    load();
    const id = setInterval(load, 15000);
    return () => { alive = false; clearInterval(id); };
  }, [serverIdsParam]);

  async function deleteWatchlist(id) {
    setWatchlist(prev => prev.filter(e => e.id !== id));
    try {
      const r = await tapeFetch(apiBase() + "/api/watchlist/" + encodeURIComponent(id), { method: "DELETE" });
      if (!r.ok) throw new Error("HTTP " + r.status);
      toast.success("Post deleted");
    } catch (err) {
      console.error("[sinux-signals] watchlist delete failed:", err);
      toast.error("Delete failed — " + err.message);
    }
  }

  // Live event source — passes serverIdsParam for filtering
  const { events, status: liveStatus, pushLocal, setEvents } = useLiveSource({
    enabled: liveCfg.enabled && !!liveCfg.endpoint,
    cfg: liveCfg,
    serverIds: serverIdsParam,
    seedEvents: [],   // no mock fallback when live
  });

  // Delete a parsed event. Positions/recaps/charts are derived from `events`,
  // so removing it here drops it from every view at once.
  async function deleteEvent(id) {
    setEvents(prev => prev.filter(e => e.id !== id));
    setFlyoutEvent(cur => (cur && cur.id === id ? null : cur));
    try {
      const r = await tapeFetch(apiBase() + "/api/events/" + encodeURIComponent(id), { method: "DELETE" });
      if (!r.ok) throw new Error("HTTP " + r.status);
      toast.success("Event deleted");
    } catch (err) {
      console.error("[sinux-signals] event delete failed:", err);
      toast.error("Delete failed — " + err.message);
    }
  }

  // Bulk delete (multi-select in the events feed).
  async function deleteEvents(ids) {
    if (!ids || !ids.length) return;
    const idSet = new Set(ids);
    setEvents(prev => prev.filter(e => !idSet.has(e.id)));
    setFlyoutEvent(cur => (cur && idSet.has(cur.id) ? null : cur));
    try {
      const r = await tapeFetch(apiBase() + "/api/events/delete", {
        method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ ids }),
      });
      if (!r.ok) throw new Error("HTTP " + r.status);
      toast.success(`Deleted ${ids.length} event${ids.length === 1 ? "" : "s"}`);
    } catch (err) {
      console.error("[sinux-signals] bulk delete failed:", err);
      toast.error("Delete failed — " + err.message);
    }
  }

  // Apply density attr (theme is handled by useTheme)
  useEffect(() => {
    document.documentElement.setAttribute("data-density", t.density);
  }, [t.density]);

  const positions = useMemo(() => aggregatePositions(events), [events]);
  const recaps    = useMemo(() => buildRecaps(events), [events]);
  const visibleEvents = useMemo(() => events.filter(e => enabled.has(e.analyst)), [events, enabled]);

  // Default the current ticker to the most active one once events load
  useEffect(() => {
    if (currentTicker) return;
    if (!events.length) return;
    const counts = new Map();
    for (const e of events) counts.set(e.ticker, (counts.get(e.ticker) || 0) + 1);
    const top = [...counts.entries()].sort((a, b) => b[1] - a[1])[0];
    if (top) setCurrentTicker(top[0]);
  }, [events, currentTicker]);

  function toggleAnalyst(handle, on) {
    setEnabled(prev => {
      const next = new Set(prev);
      if (on) next.add(handle); else next.delete(handle);
      return next;
    });
  }
  function soloAnalyst(handle) {
    setEnabled(new Set([handle]));
  }
  function openChart(ticker) {
    setCurrentTicker(ticker);
    setTab("charts");
  }

  // Counters
  const lastEventTs = events[0]?.ts;
  const activePositions = positions.filter(p => p.status !== "closed" && p.status !== "expired").length;

  const tabCounts = {
    positions: activePositions,
    events: visibleEvents.length,
    sources: enabled.size,
    charts: "—",
    watchlist: watchlist.length,
    recaps: recaps.filter(r => enabled.has(r.analyst)).length,
    settings: null,
  };

  const firstSelectedId = [...selectedServerIds][0] || null;
  const currentServer = servers.find(s => s.guild_id === firstSelectedId);

  // Role-based permissions. When auth is off (unconfigured) everyone is owner.
  const role = auth.enabled ? (auth.user?.role || "viewer") : "owner";
  const RANK = { viewer: 1, editor: 2, admin: 3, owner: 4 };
  const atLeast = (r) => (RANK[role] || 0) >= (RANK[r] || 99);
  const canAdmin = atLeast("admin");         // config: channels/analysts/servers, event deletion, audit, integrations
  const canManageUsers = atLeast("editor");  // editor+ manage users (Access)
  const canSettings = canAdmin || canManageUsers;
  const isOwner = role === "owner";
  const visibleTabs = TABS.filter(tb => tb.k !== "settings" || canSettings);
  const effectiveTab = (tab === "settings" && !canSettings) ? "positions" : tab;

  const currentTab = TABS.find(tb => tb.k === effectiveTab) || TABS[0];

  const liveLabel = liveCfg.enabled && liveCfg.endpoint
    ? (liveStatus.phase === "polling" ? "live" : liveStatus.phase === "error" ? "endpoint error" : "connecting")
    : "paused";
  const serverLabel = selectedServerIds.size === 0
    ? "no servers"
    : selectedServerIds.size === 1
    ? (currentServer?.name || "—")
    : `${selectedServerIds.size} servers`;

  // Auth gate — only when Discord login is configured on the server.
  if (!auth.checked) return <AuthSplash />;
  if (auth.enabled && auth.denied && !auth.user) return <AccessDenied reason={auth.reason} />;
  if (auth.enabled && !auth.user) return <LoginGate />;

  return (
    <div className="shell">
      {navOpen && <div className="nav-backdrop" onClick={() => setNavOpen(false)} />}
      <aside className={cx("sidebar", navOpen && "open")}>
        <div className="brand">
          <span className="mark">S</span>
          <span className="brand-name">Sinux Signals</span>
        </div>
        <nav>
          {visibleTabs.map(tb => (
            <button
              key={tb.k}
              className={cx("nav-item", tab === tb.k && "active")}
              onClick={() => { setTab(tb.k); setNavOpen(false); }}
              title={tb.title}
            >
              {I(tb.icon, { size: 17 })}
              <span className="nav-label">{tb.label}</span>
              {tabCounts[tb.k] != null && tabCounts[tb.k] !== "—" && (
                <span className="nav-badge">{tabCounts[tb.k]}</span>
              )}
            </button>
          ))}
        </nav>
        <div className="sidebar-foot">
          {auth.user && (
            <div className="user-chip">
              <Avatar user={auth.user} />
              <div className="user-meta">
                <span className="user-name">{auth.user.global_name || auth.user.username}</span>
                <span className="user-role">{role}</span>
              </div>
              <button className="icon-btn" title="Sign out" onClick={logout}>{I("logout", { size: 15 })}</button>
            </div>
          )}
          <ThemeSwitch mode={themeMode} onChange={setThemeMode} />
          <div className="sidebar-ver mono">signals · v0.5.0</div>
        </div>
      </aside>

      <div className="main">
        <header className="topbar">
          <div className="topbar-left">
            <button className="nav-toggle" title="Menu" onClick={() => setNavOpen(true)}>{I("menu", { size: 20 })}</button>
            <div className="topbar-title">
              <h1>{currentTab.title}</h1>
              <div className="topbar-sub">
              <StatusPill status={liveLabel} />
              <span className="sep">·</span>
              <span>{serverLabel}</span>
              <span className="sep">·</span>
              <span>{channels.length} channels</span>
              {lastEventTs && <><span className="sep">·</span><span>last {fmtTime(lastEventTs, {short:true})}</span></>}
              </div>
            </div>
          </div>
          <div className="topbar-actions">
            <ServerMultiSelect servers={servers} selected={selectedServerIds} onChange={setSelectedServerIds} />
          </div>
        </header>

        <div className="content">
          {/* Overview stats — only on the Positions tab to keep other views focused */}
          {tab === "positions" && (
            <div className="statgrid">
              <Stat label="Open positions" val={activePositions} delta={`+${Math.min(activePositions, 4)} this week`} icon="trend" />
              <Stat label="New entries"    val={events.filter(e=>e.action==="open" && e.ts>=Date.now()-86400000).length} delta="last 24h" icon="plus" />
              <Stat label="Trims today"    val={events.filter(e=>e.action==="trim" && e.ts>=Date.now()-86400000).length} delta="locking gains" icon="trendDown" />
              <Stat label="Closed this week" val={recaps.filter(r=>r.updatedTs>=Date.now()-7*86400000).length} delta={`${recaps.filter(r=>r.updatedTs>=Date.now()-7*86400000 && r.pnlPct>=0).length}W / ${recaps.filter(r=>r.updatedTs>=Date.now()-7*86400000 && r.pnlPct<0).length}L`} icon="recap" />
            </div>
          )}

          {tab === "positions" && (
            <div className="filterbar">
              <input className="input search" placeholder="Filter by analyst or ticker…" value={filter} onChange={e => setFilter(e.target.value)} />
              <div className="numinput">
                <label>days back</label>
                <input type="number" min="1" max="365" value={days} onChange={e => setDays(Math.max(1, Number(e.target.value) || 1))} />
              </div>
              <div className="numinput">
                <label>min trades</label>
                <input type="number" min="1" max="20" value={minTrades} onChange={e => setMinTrades(Math.max(1, Number(e.target.value) || 1))} />
              </div>
              <button className="btn" onClick={() => { setFilter(""); setDays(14); setMinTrades(1); }}>{I("close", { size: 12 })} Reset</button>
            </div>
          )}

          {tab === "recaps" && (
            <div className="filterbar" style={{ gridTemplateColumns: "1fr auto" }}>
              <input className="input search" placeholder="Filter recaps by ticker or analyst…" value={filter} onChange={e => setFilter(e.target.value)} />
              <button className="btn" onClick={() => setFilter("")}>{I("close", { size: 12 })} Clear</button>
            </div>
          )}

          {/* each tab in its own ErrorBoundary so one crash doesn't blank the app.
              key={tab} forces a fresh boundary on tab change so a prior crash doesn't stick. */}
          <ErrorBoundary key={tab}>
            {tab === "positions" && (
              <div className="split cols">
                <Positions
                  positions={positions} events={events} analysts={analysts}
                  filter={filter} days={days} minTrades={minTrades}
                  onPickTicker={setCurrentTicker} onOpenChart={openChart}
                  showParser={isOwner}
                />
                <EventRail events={visibleEvents} analysts={analysts} onClickEvent={setFlyoutEvent} />
              </div>
            )}
            {tab === "events" && (
              <SignalEvents events={events} analysts={analysts} onClickEvent={setFlyoutEvent} onDelete={canAdmin ? deleteEvent : undefined} onDeleteMany={canAdmin ? deleteEvents : undefined} />
            )}
            {tab === "sources" && (
              <Sources analysts={analysts} events={events} enabled={enabled} onToggle={toggleAnalyst} onSolo={soloAnalyst} />
            )}
            {tab === "charts" && (
              <ChartsTab events={events} analysts={analysts} enabled={enabled} onToggle={toggleAnalyst}
                currentTicker={currentTicker || "NVDA"} setCurrentTicker={setCurrentTicker} onClickEvent={setFlyoutEvent} />
            )}
            {tab === "watchlist" && (
              <Watchlist entries={watchlist} analysts={analysts} onDelete={canAdmin ? deleteWatchlist : undefined} />
            )}
            {tab === "recaps" && (
              <Recaps recaps={recaps} analysts={analysts} enabled={enabled} filter={filter} />
            )}
            {tab === "settings" && canSettings && (
              <Settings
                channels={channels} setChannels={setChannels}
                analysts={analysts} setAnalysts={setAnalysts}
                events={events}
                servers={servers} setServers={setServers} currentServerId={firstSelectedId}
                onSelectServer={(id) => setSelectedServerIds(prev => new Set([...prev, id]))}
                liveStatus={liveStatus}
                canAdmin={canAdmin} canManageUsers={canManageUsers} isOwner={isOwner} role={role}
              />
            )}
          </ErrorBoundary>
        </div>

        <footer className="app-footer mono">
          <span className="app-footer-copy">© {new Date().getFullYear()} Sinux Consulting. All rights reserved.</span>
          <span className="app-footer-mid">
            Developed by <a href="https://www.sinuxconsulting.com" target="_blank" rel="noopener noreferrer">Sinux Consulting</a>
          </span>
          <span className="app-footer-ver">Sinux Signals v0.5.0</span>
        </footer>
      </div>

      {flyoutEvent && <EventFlyout event={flyoutEvent} onClose={() => setFlyoutEvent(null)} onDelete={canAdmin ? deleteEvent : undefined} />}

      <TweaksPanel title="Tweaks">
        <TweakSection title="Layout">
          <TweakRadio label="Density" value={t.density} onChange={v => setTweak("density", v)}
            options={[
              { value: "compact",     label: "Tight" },
              { value: "default",     label: "Std" },
              { value: "comfortable", label: "Roomy" },
            ]} />
        </TweakSection>
        <TweakSection title="Parser detail">
          <TweakToggle label="Expand parser internals" value={t.showParser} onChange={v => setTweak("showParser", v)} />
        </TweakSection>
      </TweaksPanel>

      <ConfirmHost />
      <ToastHost />
    </div>
  );
}

function Stat({ label, val, delta, icon, mono }) {
  const isUp = String(delta).startsWith("+");
  return (
    <div className="stat">
      <div className="lbl">{I(icon, { size: 11 })} {label}</div>
      <div className="val">{val}</div>
      <div className={cx("delta", isUp && "up")}>{delta}</div>
    </div>
  );
}

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