// Root + App.
//
// Bootstrap probes the bridge (api.jsx). If reachable, the UI runs against
// live BE RCON via /api. If not (project preview, offline), it falls back
// to mock data from data.jsx so the prototype still works.

// --- Live <-> mock data shape helpers -------------------------------------
function detectPlatform(guid) {
  if (!guid) return "unknown";
  if (/^7656119\d{10}$/.test(guid)) return "steam";
  // BE RCON GUIDs are 32-char MD5 hex — platform can't be inferred reliably;
  // adjust here if your hosting tags console players with a recognizable prefix.
  return "unknown";
}
function normalizePlayer(p, serverId) {
  return {
    id: `${serverId}_${p.guid || p.slot}`,
    slot: p.slot,
    name: p.name,
    platform: p.platform || detectPlatform(p.guid),
    guid: p.guid,
    ip: p.ip,
    ping: p.ping,
    sessionMin: 0,
    faction: "—",
    kills: 0, deaths: 0, score: 0,
    isAdmin: false,
    flagged: p.status && p.status !== "OK",
  };
}
function normalizeServer(s) {
  return {
    id: s.id, name: s.name,
    region: s.region, map: s.map, mode: s.mode,
    maxPlayers: s.maxPlayers,
    status: s.status,
    error: s.error,
    players: (s.players || []).map(p => normalizePlayer(p, s.id)),
    kills: (s.kills || []).map(k => normalizeKill(k, s.id)),
    uptimeHrs: s.uptimeHrs || 0,
    fps: s.fps || 0,
    memMb: s.memMb || 0,
    cpu: s.cpu || 0,
    ramGB: s.ramGB || 0,
    spark: s.spark || [],
  };
}
// Normalize a live kill (from the log feed) into a UI-friendly shape.
function normalizeKill(k, serverId) {
  return {
    id: `${serverId}_k_${k.ts}_${k.victimGuid || k.victim}`,
    ts: k.ts,
    suicide: !!k.suicide,
    killerName: k.killer || "—",
    killerGuid: k.killerGuid || null,
    victimName: k.victim || "—",
    victimGuid: k.victimGuid || null,
    weapon: k.weapon || "—",
    hitzone: k.hitzone || null,
    faction: k.victimFaction || null,
  };
}
function normalizeAuditRow(row) {
  return {
    id: row.id, ts: row.ts,
    who: row.who, action: row.action,
    server: row.server || row.server_id || "",
    target: row.target, detail: row.detail,
  };
}

// --- Live data hook -------------------------------------------------------
function useLiveData(enabled, addToast) {
  const [servers, setServers] = React.useState([]);
  const [audit, setAudit] = React.useState([]);
  const [bans, setBans] = React.useState([]);
  const [notes, setNotes] = React.useState({});  // keyed by guid
  const [lastSync, setLastSync] = React.useState(null);
  const refreshRef = React.useRef(null);

  React.useEffect(() => {
    if (!enabled) return;
    let cancelled = false;

    const refresh = async () => {
      try {
        const [s, a] = await Promise.all([API.servers(), API.audit()]);
        if (cancelled) return;
        setServers(s.map(normalizeServer));
        setAudit(a.map(normalizeAuditRow));
        setLastSync(Date.now());
      } catch (e) {
        if (e.status === 401) { location.reload(); return; }
        console.warn("refresh failed:", e.message);
      }
    };
    refreshRef.current = refresh;
    refresh();
    const t = setInterval(refresh, 5000);

    let ws;
    try {
      ws = API.subscribe((msg) => {
        if (msg.type === "players") {
          setServers(prev => prev.map(s =>
            s.id === msg.serverId
              ? { ...s, players: msg.payload.map(p => normalizePlayer(p, s.id)) }
              : s));
        } else if (msg.type === "audit") {
          setAudit(prev => [normalizeAuditRow(msg.payload), ...prev]);
        } else if (msg.type === "kill") {
          setServers(prev => prev.map(s =>
            s.id === msg.serverId
              ? { ...s, kills: [...(s.kills || []), normalizeKill(msg.payload, s.id)].slice(-200) }
              : s));
        } else if (msg.type === "telemetry") {
          setServers(prev => prev.map(s =>
            s.id === msg.serverId
              ? { ...s, fps: msg.payload.fps, memMb: msg.payload.memMb }
              : s));
        } else if (msg.type === "status") {
          setServers(prev => prev.map(s =>
            s.id === msg.payload.id
              ? { ...s, status: msg.payload.connected ? "online" : "offline", error: msg.payload.error }
              : s));
        }
      });
    } catch (e) { console.warn("ws subscribe failed:", e.message); }

    return () => {
      cancelled = true;
      clearInterval(t);
      try { ws?.close?.(); } catch {}
    };
  }, [enabled]);

  // Lazy-load notes when a guid is requested
  const loadNotesFor = React.useCallback(async (guid) => {
    if (!enabled || !guid) return;
    try {
      const rows = await API.notes(guid);
      setNotes(prev => ({
        ...prev,
        [guid]: rows.map(r => ({ id: r.id, body: r.body, author: r.author, ts: r.ts }))
      }));
    } catch (e) { console.warn("notes load failed:", e.message); }
  }, [enabled]);

  return { servers, audit, bans, notes, setNotes, loadNotesFor, setServers, setAudit, lastSync, refresh: () => refreshRef.current && refreshRef.current() };
}

// --- Root ---------------------------------------------------------------
const Root = () => {
  const [boot, setBoot] = React.useState(null);
  React.useEffect(() => { probe().then(setBoot); }, []);

  if (!boot) {
    return (
      <div style={{ position: "fixed", inset: 0, display: "grid", placeItems: "center", color: "var(--muted)", fontSize: 12 }}>
        Connecting…
      </div>
    );
  }
  if (boot.mode === "live" && !boot.user) {
    return <Login onLogin={(user) => setBoot({ mode: "live", user })} />;
  }
  const user = boot.mode === "live"
    ? { ...boot.user, initials: (boot.user.handle || "??").slice(0, 2).toUpperCase() }
    : null;  // mock mode → AuthProvider uses its default mock user
  return (
    <AuthProvider user={user} initialUser="mara">
      <ToastProvider>
        <App
          live={boot.mode === "live"}
          onLogout={async () => {
            try { await API.logout(); } catch {}
            setBoot({ mode: "live", user: null });
          }}
        />
      </ToastProvider>
    </AuthProvider>
  );
};

// --- App ----------------------------------------------------------------
const App = ({ live, onLogout }) => {
  const auth = useAuth();
  const toast = useToast();

  const [view, setView] = React.useState("dash");
  const [activeServerId, setActiveServerId] = React.useState(null);
  const [selected, setSelected] = React.useState(null);
  const [search, setSearch] = React.useState("");
  const [modal, setModal] = React.useState(null);

  const liveData = useLiveData(live, toast);

  // Mock data shadow state (only used when !live)
  const [mockServers] = React.useState(DATA.SERVERS);
  const [mockAudit, setMockAudit] = React.useState(DATA.AUDIT);
  const [mockNotes, setMockNotes] = React.useState(DATA.NOTES);
  const [bannedIds, setBannedIds] = React.useState(new Set());

  const servers = live ? liveData.servers : mockServers;
  const audit = live ? liveData.audit : mockAudit;
  const notes = live ? liveData.notes : mockNotes;

  const activeServer = servers.find(s => s.id === activeServerId);

  // In live mode, load notes when player is selected.
  React.useEffect(() => {
    if (live && selected?.guid) liveData.loadNotesFor(selected.guid);
  }, [live, selected?.guid]);

  // Roster filter: in mock mode we hide kicked/banned ids.
  const visiblePlayers = React.useMemo(() => {
    if (!activeServer) return [];
    if (live) return activeServer.players;
    return activeServer.players.filter(p => !bannedIds.has(p.id));
  }, [activeServer, bannedIds, live]);

  const totals = React.useMemo(() => ({
    online: servers.reduce((a,s) => a + (s.players?.length || 0), 0),
    cap: servers.reduce((a,s) => a + (s.maxPlayers || 0), 0),
    servers: `${servers.filter(s => s.status !== "offline").length}/${servers.length}`
  }), [servers]);

  // Global player search (used on the dashboard when a query is typed).
  const globalMatches = React.useMemo(() => {
    const q = search.trim().toLowerCase();
    if (!q) return [];
    const res = [];
    for (const s of servers) {
      for (const p of (s.players || [])) {
        if (p.name.toLowerCase().includes(q) ||
            (p.guid || "").toLowerCase().includes(q) ||
            (p.ip || "").includes(q)) {
          res.push({ server: s, player: p });
        }
      }
    }
    return res;
  }, [servers, search]);

  const crumbs = React.useMemo(() => {
    if (view === "audit") return ["Staff audit"];
    if (view === "feed") return ["Live chat"];
    if (view === "bans") return ["Bans"];
    if (activeServer) return ["Servers", activeServer.name];
    return ["Servers"];
  }, [view, activeServer]);

  const onCrumb = (i) => { if (i === 0 && activeServer) { setActiveServerId(null); setSelected(null); } };
  const openServer = (id) => { setActiveServerId(id); setSelected(null); setSearch(""); };
  const back = () => { setActiveServerId(null); setSelected(null); };

  const logActionMock = (action, target, detail, serverId) => {
    setMockAudit(prev => [{
      id: "a" + Math.random().toString(36).slice(2),
      ts: Date.now(),
      who: auth.user.handle,
      action, target, detail,
      server: serverId || (activeServer ? activeServer.id : "")
    }, ...prev]);
  };

  // Notes
  const addNote = async (playerId, body) => {
    const p = activeServer?.players.find(x => x.id === playerId);
    if (!p) return;
    if (live) {
      try {
        await API.addNote(p.guid, body);
        await liveData.loadNotesFor(p.guid);
        toast(`Note added to ${p.name}`, "ok");
      } catch (e) { toast(e.message, "bad"); }
    } else {
      setMockNotes(prev => ({
        ...prev,
        [playerId]: [{ id: "n" + Math.random().toString(36).slice(2), body, author: auth.user.handle, ts: Date.now() }, ...(prev[playerId] || [])]
      }));
      logActionMock("added note", p.name, body.length > 60 ? body.slice(0,60)+"…" : body);
      toast(`Note added to ${p.name}`, "ok");
    }
  };

  // -- Action confirmers --
  const confirmKick = async (reason) => {
    const p = modal.player;
    if (live) {
      try {
        await API.kick(activeServer.id, p.slot ?? p.guid, reason);
        toast(`${p.name} kicked`, "warn");
      } catch (e) { toast(e.message, "bad"); }
    } else {
      setBannedIds(prev => new Set([...prev, p.id]));
      logActionMock("kicked", p.name, `Reason: ${reason}`);
      toast(`${p.name} kicked`, "warn");
    }
    if (selected?.id === p.id) setSelected(null);
    setModal(null);
  };

  const confirmBan = async ({ mode, duration, scope, reason }) => {
    const p = modal.player;
    const minutesMap = { "1h":60, "6h":360, "24h":1440, "3d":4320, "7d":10080, "30d":43200 };
    const minutes = mode === "perm" ? 0 : minutesMap[duration] || 60;
    if (live) {
      try {
        await API.ban(activeServer.id, {
          target: p.slot ?? p.guid,
          minutes,
          reason,
          scope: scope === "all" ? "network" : "server",
          playerName: p.name,
          playerGuid: p.guid
        });
        toast(`${p.name} ${mode === "perm" ? "permanently banned" : "banned for " + duration}`, "bad");
      } catch (e) { toast(e.message, "bad"); }
    } else {
      setBannedIds(prev => new Set([...prev, p.id]));
      logActionMock(mode === "perm" ? "permbanned" : "tempbanned", p.name,
        `${mode === "temp" ? `Duration ${duration}` : "Permanent"} · scope ${scope === "all" ? "network" : "this server"}${reason ? " · " + reason : ""}`);
      toast(`${p.name} ${mode === "perm" ? "permanently banned" : "banned for " + duration}`, "bad");
    }
    if (selected?.id === p.id) setSelected(null);
    setModal(null);
  };

  const confirmPM = async (msg) => {
    const p = modal.player;
    if (live) {
      try { await API.pm(activeServer.id, p.slot ?? p.guid, msg); toast(`Message sent to ${p.name}`, "ok"); }
      catch (e) { toast(e.message, "bad"); }
    } else {
      logActionMock("messaged", p.name, `“${msg}”`);
      toast(`Message sent to ${p.name}`, "ok");
    }
    setModal(null);
  };

  const confirmBroadcast = async ({ msg, scope }) => {
    if (live) {
      try {
        await API.broadcast(activeServer.id, msg, scope === "all" ? "network" : "server");
        toast(`Broadcast sent${scope === "all" ? " to all 5 servers" : ""}`, "ok");
      } catch (e) { toast(e.message, "bad"); }
    } else {
      if (scope === "all") {
        servers.forEach(s => logActionMock("broadcast", null, `“${msg}”`, s.id));
      } else {
        logActionMock("broadcast", null, `“${msg}”`);
      }
      toast(`Broadcast sent${scope === "all" ? " to all 5 servers" : ""}`, "ok");
    }
    setModal(null);
  };

  const confirmPower = async ({ mode, delay, reason }) => {
    if (live) {
      try {
        // Optional: warn players first
        if (delay > 0 && reason) {
          try { await API.broadcast(activeServer.id, `${mode === "restart" ? "Restart" : "Shutdown"} in ${delay}m — ${reason}`, "server"); } catch {}
        }
        await API.power(activeServer.id, mode, reason);
        toast(`${mode === "restart" ? "Restart" : "Shutdown"} command sent`, "warn");
      } catch (e) { toast(e.message, "bad"); }
    } else {
      logActionMock("restarted", null, `${mode === "restart" ? "Rolling restart" : "Shutdown"} in ${delay}m${reason ? " · " + reason : ""}`);
      toast(`${mode === "restart" ? "Restart" : "Shutdown"} scheduled in ${delay}m`, "warn");
    }
    setModal(null);
  };

  if (live && !servers.length) {
    return (
      <div style={{ position: "fixed", inset: 0, display: "grid", placeItems: "center", color: "var(--muted)", fontSize: 12 }}>
        Loading servers…
      </div>
    );
  }

  return (
    <div className="app">
      <Sidebar
        view={activeServer ? "dash" : view}
        setView={(v) => { setActiveServerId(null); setSelected(null); setView(v); }}
        live={live}
        onLogout={onLogout}
        onSettings={() => setModal({ type: "settings" })}
      />
      <div className="main">
        <TopBar
          crumbs={crumbs}
          onCrumb={onCrumb}
          search={search}
          setSearch={setSearch}
          totals={totals}
          live={live}
        />
        <div className="content" key={view + (activeServerId || "")}>
          {!activeServer && view === "dash" && search.trim() && (
            <div>
              <div className="dash-head">
                <div>
                  <div className="dash-title">Search</div>
                  <div className="dash-sub">{globalMatches.length} player{globalMatches.length === 1 ? "" : "s"} matching “{search.trim()}”</div>
                </div>
              </div>
              <div className="sr-list">
                {globalMatches.map(({ server: s, player: p }) => (
                  <button key={s.id + "_" + p.id} className="sr-row" onClick={() => { openServer(s.id); setSelected(p); }}>
                    <span className="flex1">
                      <span className="name">{p.name}</span>{" "}
                      <span className="muted mono" style={{ fontSize: 11 }}>{auth.maskGUID(p.guid)}</span>
                    </span>
                    <span className="muted">{s.name}</span>
                  </button>
                ))}
                {globalMatches.length === 0 && (
                  <div className="muted" style={{ fontSize: 12, padding: "12px 2px" }}>No players match.</div>
                )}
              </div>
            </div>
          )}
          {!activeServer && view === "dash" && !search.trim() && (
            <Dashboard
              servers={servers}
              onOpen={openServer}
              audit={audit}
              live={live}
              onRefresh={live ? liveData.refresh : undefined}
              lastSync={live ? liveData.lastSync : null}
            />
          )}
          {!activeServer && view === "audit" && (
            <GlobalAudit audit={audit} servers={servers} />
          )}
          {!activeServer && view === "feed" && (
            <GlobalChat servers={servers} live={live} />
          )}
          {!activeServer && view === "bans" && (
            <BansView audit={audit} servers={servers} />
          )}
          {activeServer && (
            <ServerDetail
              server={activeServer}
              players={visiblePlayers}
              search={search}
              live={live}
              selected={selected}
              setSelected={setSelected}
              openModal={setModal}
              audit={audit}
              notes={live
                ? Object.fromEntries(visiblePlayers.map(p => [p.id, notes[p.guid] || []]))
                : notes}
              addNote={addNote}
              onBack={back}
              onBroadcast={() => setModal({ type: "broadcast" })}
              onPower={() => setModal({ type: "power" })}
            />
          )}
        </div>
      </div>

      {modal?.type === "kick" && (
        <KickModal player={modal.player} server={activeServer} onClose={() => setModal(null)} onConfirm={confirmKick} />
      )}
      {modal?.type === "ban" && (
        <BanModal player={modal.player} server={activeServer} onClose={() => setModal(null)} onConfirm={confirmBan} />
      )}
      {modal?.type === "pm" && (
        <PMModal player={modal.player} server={activeServer} onClose={() => setModal(null)} onConfirm={confirmPM} />
      )}
      {modal?.type === "broadcast" && (
        <BroadcastModal server={activeServer} onClose={() => setModal(null)} onConfirm={confirmBroadcast} />
      )}
      {modal?.type === "power" && (
        <PowerModal server={activeServer} onClose={() => setModal(null)} onConfirm={confirmPower} />
      )}
      {modal?.type === "settings" && (
        <SettingsModal live={live} onClose={() => setModal(null)} />
      )}
    </div>
  );
};

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