// HTTP client for the MarketMaker server — Monitors build. Only the auth,
// settings, monitors, and WebSocket surface is kept; a viewer key is scoped
// server-side to /api/monitors (+ /api/settings), so any other method would 403
// anyway — omitting them keeps the bundle free of non-monitor endpoint knowledge.
// Session = access key in localStorage. All calls go through one request().

const LS_KEY = "mm-key";
const LS_URL = "mm-server";
const LS_USER = "mm-user";

const Api = {
  getServerUrl() {
    return localStorage.getItem(LS_URL) || window.MM_CONFIG.defaultServerUrl;
  },
  setServerUrl(url) { localStorage.setItem(LS_URL, url); },

  getKey() { return localStorage.getItem(LS_KEY) || ""; },
  setKey(key) {
    if (key) localStorage.setItem(LS_KEY, key);
    else localStorage.removeItem(LS_KEY);
  },

  getCachedUser() {
    try { return JSON.parse(localStorage.getItem(LS_USER) || "null"); }
    catch { return null; }
  },
  setCachedUser(user) {
    if (user) localStorage.setItem(LS_USER, JSON.stringify(user));
    else localStorage.removeItem(LS_USER);
  },

  async request(method, path, body) {
    const hadKey = !!this.getKey();
    const res = await fetch(this.getServerUrl() + path, {
      method,
      headers: {
        "Content-Type": "application/json",
        Authorization: "Bearer " + this.getKey(),
      },
      body: body != null ? JSON.stringify(body) : undefined,
    });
    if (res.status === 204) return null;
    const data = await res.json().catch(() => ({}));
    if (!res.ok) {
      // Token rejected by the server (key revoked / user deleted / expired):
      // clear the stored key and notify the app so it can bounce to /login.
      // Skip if we never had a key — that's just a plain login failure.
      if (res.status === 401 && hadKey) {
        this.setKey("");
        this.setCachedUser(null);
        this._emit("auth.expired", { reason: data.error || "unauthorized" });
      }
      const err = new Error(data.error || `HTTP ${res.status}`);
      err.status = res.status;
      err.data = data;
      throw err;
    }
    return data;
  },

  // ----- auth -----
  async login(key) {
    this.setKey(key);
    const { user } = await this.request("POST", "/api/auth/login");
    this.setCachedUser(user);
    return user;
  },
  async refreshMe() {
    const { user } = await this.request("GET", "/api/auth/me");
    this.setCachedUser(user);
    return user;
  },
  logout() { this.setKey(""); this.setCachedUser(null); },

  // ----- user settings (theme / auto-lock — allowlisted for viewers) -----
  getSettings() { return this.request("GET", "/api/settings"); },
  updateSettings(body) { return this.request("PATCH", "/api/settings", body); },
  getMonitorPresets() { return this.request("GET", "/api/settings/monitor-presets"); },
  saveMonitorPresets(value) { return this.request("PUT", "/api/settings/monitor-presets", value); },

  // ----- monitors (pump.fun token research) -----
  //
  // The /api/monitors/* responses come straight out of Postgres, where NUMERIC
  // and BIGINT columns arrive as strings (pg's lossless default). The Monitors
  // pages call .toFixed()/arithmetic on these fields directly, so _numify walks
  // a response and turns numeric-looking strings back into numbers. `balance` /
  // `symbol` / `name` stay strings on purpose (balances can exceed 2^53;
  // symbol/name are free text that may be all digits).
  _monKeepString: new Set(["balance", "symbol", "name"]),
  _numify(v, key) {
    if (typeof v === "string") {
      if (this._monKeepString.has(key)) return v;
      return /^-?\d+(?:\.\d+)?$/.test(v) ? Number(v) : v;
    }
    if (Array.isArray(v)) return v.map((x) => this._numify(x, key));
    if (v && typeof v === "object") {
      const out = {};
      for (const k in v) out[k] = this._numify(v[k], k);
      return out;
    }
    return v;
  },
  // Build a query string, dropping null / undefined / "" so an unset cohort
  // filter is simply absent (sending athMin="" would fail server validation).
  _monQs(params) {
    const u = new URLSearchParams();
    for (const [k, v] of Object.entries(params || {})) {
      if (v == null || v === "") continue;
      u.set(k, v);
    }
    const s = u.toString();
    return s ? "?" + s : "";
  },
  listMonitors(params = {}) {
    return this.request("GET", "/api/monitors" + this._monQs(params)).then((d) => this._numify(d));
  },
  getMonitor(id) {
    return this.request("GET", "/api/monitors/" + id).then((d) => this._numify(d));
  },
  getMonitorSeries(id, params = {}) {
    return this.request("GET", "/api/monitors/" + id + "/series" + this._monQs(params))
      .then((d) => this._numify(d));
  },
  getMonitorHolders(id, kind) {
    return this.request("GET", "/api/monitors/" + id + "/holders" + this._monQs({ kind }))
      .then((d) => this._numify(d));
  },
  getMonitorVolCurve(id) {
    return this.request("GET", "/api/monitors/" + id + "/vol-curve");
  },
  getMonitorHolderCurve(id) {
    return this.request("GET", "/api/monitors/" + id + "/holder-curve");
  },
  getMonitorTxCurve(id) {
    return this.request("GET", "/api/monitors/" + id + "/tx-curve");
  },
  getMonitorPnlCurve(id) { return this.request("GET", "/api/monitors/" + id + "/pnl-curve"); },
  getMonitorSupplyCurve(id) { return this.request("GET", "/api/monitors/" + id + "/supply-curve"); },
  getMonitorFeeCurve(id) { return this.request("GET", "/api/monitors/" + id + "/fee-curve"); },
  getMonitorPlatformTxCurve(id) { return this.request("GET", "/api/monitors/" + id + "/platform-tx-curve"); },
  getMonitorAttention(id) {
    return this.request("GET", "/api/monitors/" + id + "/attention").then((d) => this._numify(d));
  },
  getMonitorLatestAttention(id) {
    return this.request("GET", "/api/monitors/" + id + "/attention/latest").then((d) => this._numify(d));
  },
  getMonitorLiveHolders(id) {
    return this.request("GET", "/api/monitors/" + id + "/live-holders").then((d) => this._numify(d));
  },
  getMonitorTrades(id, limit = 200) {
    return this.request("GET", "/api/monitors/" + id + "/trades?limit=" + limit);
  },
  getMonitorEvents(id) {
    return this.request("GET", "/api/monitors/" + id + "/events").then((d) => this._numify(d));
  },
  getMonitorByMint(mint) {
    return this.request("GET", "/api/monitors/by-mint/" + encodeURIComponent(mint)).then((d) => this._numify(d));
  },
  activateMonitor(mint) {
    return this.request("POST", "/api/monitors/by-mint/" + encodeURIComponent(mint) + "/activate").catch(() => {});
  },
  listMonitorPlatforms() {
    return this.request("GET", "/api/monitors/platforms").then((d) => this._numify(d));
  },
  getMonitorStats() {
    return this.request("GET", "/api/monitors/stats").then((d) => this._numify(d));
  },
  listMonitorIds(params = {}) {
    return this.request("GET", "/api/monitors/ids" + this._monQs(params)).then((d) => this._numify(d));
  },
  runResearchAnalysis(ids, opts = {}) {
    return this.request("POST", "/api/monitors/research/analyze", {
      ids, target: opts.target, modules: opts.modules, minRows: opts.minRows,
    });
  },

  // ----- realtime push -----
  //
  // Single shared WebSocket per session. Pages subscribe to event types via
  // Api.on(type, cb). The socket auto-reconnects with backoff while a valid
  // access key is stored. A viewer socket only receives monitor.* events
  // (gated server-side in ws/broadcast).

  _socket: null,
  _listeners: new Map(),   // type → Set<callback>
  _reconnectMs: 1000,
  _reconnectTimer: null,
  _connectRequested: false,

  _streamLag: null,
  _pingTimer: null,
  getStreamLag() { return this._streamLag; },

  on(type, cb) {
    if (!this._listeners.has(type)) this._listeners.set(type, new Set());
    this._listeners.get(type).add(cb);
    return () => this.off(type, cb);
  },
  off(type, cb) {
    this._listeners.get(type)?.delete(cb);
  },
  _emit(type, payload) {
    this._listeners.get(type)?.forEach((cb) => {
      try { cb(payload); } catch (e) { console.error("[ws bus]", type, e); }
    });
    // also emit a wildcard so pages can log all events for debugging
    this._listeners.get("*")?.forEach((cb) => cb({ type, ...payload }));
  },

  connect() {
    this._connectRequested = true;
    if (!this.getKey()) return; // no session, nothing to connect to
    if (this._socket && this._socket.readyState <= 1) return; // already (re)connecting

    const url = this.getServerUrl().replace(/^http/, "ws") + "/ws";
    const ws = new WebSocket(url);
    this._socket = ws;

    ws.addEventListener("open", () => {
      this._reconnectMs = 1000;
      ws.send(JSON.stringify({ type: "auth", key: this.getKey() }));
      // Start round-trip probe (server echoes the client timestamp back).
      const ping = () => {
        if (ws.readyState !== WebSocket.OPEN) return;
        try { ws.send(JSON.stringify({ type: "ping", t: Date.now() })); } catch {}
      };
      ping();
      clearInterval(this._pingTimer);
      this._pingTimer = setInterval(ping, 5000);
    });
    ws.addEventListener("message", (e) => {
      let msg;
      try { msg = JSON.parse(e.data); } catch { return; }
      if (!msg || typeof msg.type !== "string") return;
      if (msg.type === "pong" && typeof msg.t === "number") {
        this._streamLag = Math.max(0, Date.now() - msg.t);
        this._emit("stream.lag", { ms: this._streamLag });
        return;
      }
      const { type, ...rest } = msg;
      this._emit(type, rest);
    });
    ws.addEventListener("close", () => {
      this._socket = null;
      this._streamLag = null;
      clearInterval(this._pingTimer);
      this._pingTimer = null;
      this._emit("stream.lag", { ms: null });
      if (!this._connectRequested) return;
      // exponential backoff up to 30s
      clearTimeout(this._reconnectTimer);
      this._reconnectTimer = setTimeout(() => this.connect(), this._reconnectMs);
      this._reconnectMs = Math.min(this._reconnectMs * 2, 30_000);
    });
    ws.addEventListener("error", () => { /* close fires after — handle there */ });
    return ws;
  },

  disconnect() {
    this._connectRequested = false;
    clearTimeout(this._reconnectTimer);
    clearInterval(this._pingTimer);
    this._pingTimer = null;
    this._streamLag = null;
    if (this._socket) {
      try { this._socket.close(); } catch {}
      this._socket = null;
    }
  },
};

window.Api = Api;
