// OrbitCosmo Claude client — the one module allowed to make a network call.
// Everything the wiki sends leaves through callClaude(), on one of two
// routes chosen in settings:
//
//   proxy (default) — OrbitCosmo's capped Cloudflare Worker (proxy/), which holds
//     the real Anthropic key server-side and enforces a monthly spend cap.
//     The extension sends only a per-tester bearer token.
//   byo — the user's own Anthropic key, straight to api.anthropic.com from
//     the service worker. The key lives in chrome.storage.local only — never
//     synced off-device, never sent to any OrbitCosmo server.
//
// Both hosts are allowlisted in the manifest CSP (connect-src) — nothing
// else in the extension can reach the network. Non-streaming by design:
// responses are structured-output JSON, and the pending fetch keeps the MV3
// worker alive for the call's duration.

(function (root) {
  "use strict";

  // Placeholder until the proxy is deployed (proxy/README.md); BYO works
  // regardless. Must match the manifest CSP entry when it changes.
  const PROXY_URL = "https://orbit-proxy.orbitcosmo.workers.dev";
  const ANTHROPIC_URL = "https://api.anthropic.com";
  const ANTHROPIC_VERSION = "2023-06-01";
  const DEFAULT_MODEL = "claude-haiku-4-5";
  const MODELS = ["claude-haiku-4-5", "claude-sonnet-4-6"];
  const DEFAULT_MAX_TOKENS = 4096;
  const DEFAULT_CAP_USD = 3;

  // $/MTok — used only for the local spend estimate that backs the client-
  // side monthly cap (the proxy enforces its own authoritative cap).
  const PRICES = {
    "claude-haiku-4-5": { inPerM: 1, outPerM: 5 },
    "claude-sonnet-4-6": { inPerM: 3, outPerM: 15 },
  };

  const SETTINGS_DEFAULTS = {
    wikiEnabled: false,
    llmRoute: "proxy", // "proxy" | "byo"
    proxyToken: "",
    anthropicKey: "",
    model: DEFAULT_MODEL,
    monthlyCapUsd: DEFAULT_CAP_USD,
  };

  async function getSettings() {
    const got = await chrome.storage.local.get("orbit_settings");
    return Object.assign({}, SETTINGS_DEFAULTS, got.orbit_settings || {});
  }

  function monthKey(now) {
    const d = new Date(now);
    return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}`;
  }

  function estUsd(model, inTok, outTok) {
    const p = PRICES[model] || PRICES[DEFAULT_MODEL];
    return (inTok * p.inPerM + outTok * p.outPerM) / 1e6;
  }

  // Read + roll the spend ledger in wikiState (resets on month change).
  async function readSpend(now) {
    const state = (await dbGet("wikiState", "state")) || { id: "state", ingested: {} };
    const mk = monthKey(now);
    if (!state.spend || state.spend.month !== mk) {
      state.spend = { month: mk, inTok: 0, outTok: 0, estUsd: 0 };
    }
    return state;
  }

  async function recordUsage(model, usage, now) {
    const state = await readSpend(now);
    state.spend.inTok += usage.input_tokens || 0;
    state.spend.outTok += usage.output_tokens || 0;
    state.spend.estUsd += estUsd(model, usage.input_tokens || 0, usage.output_tokens || 0);
    await dbPut("wikiState", state);
    return state.spend;
  }

  // One call, structured output, typed result — never throws into callers:
  // { ok:true, data, usage } or { ok:false, code, message } with code in
  // "disabled" | "no-key" | "cap" | "network" | "http-<n>" | "parse" | "truncated".
  async function callClaude({ system, messages, outputSchema, maxTokens, model }) {
    const s = await getSettings();
    if (!s.wikiEnabled) return { ok: false, code: "disabled", message: "The wiki is turned off in settings." };

    const chosenModel = MODELS.includes(model || s.model) ? (model || s.model) : DEFAULT_MODEL;
    const now = Date.now();
    const state = await readSpend(now);
    if (state.spend.estUsd >= (s.monthlyCapUsd || DEFAULT_CAP_USD)) {
      return { ok: false, code: "cap", message: "This month's wiki budget is used up. It resets next month, or raise the cap in settings." };
    }

    let url, headers;
    if (s.llmRoute === "byo") {
      if (!s.anthropicKey) return { ok: false, code: "no-key", message: "Add your Anthropic API key in settings." };
      url = `${ANTHROPIC_URL}/v1/messages`;
      headers = {
        "content-type": "application/json",
        "x-api-key": s.anthropicKey,
        "anthropic-version": ANTHROPIC_VERSION,
        "anthropic-dangerous-direct-browser-access": "true",
      };
    } else {
      if (!s.proxyToken) return { ok: false, code: "no-key", message: "Add your OrbitCosmo beta token in settings (or switch to your own API key)." };
      url = `${PROXY_URL}/v1/messages`;
      headers = {
        "content-type": "application/json",
        authorization: `Bearer ${s.proxyToken}`,
      };
    }

    const body = {
      model: chosenModel,
      max_tokens: maxTokens || DEFAULT_MAX_TOKENS,
      system,
      messages,
    };
    if (outputSchema) {
      body.output_config = { format: { type: "json_schema", schema: outputSchema } };
    }

    let resp;
    try {
      resp = await fetch(url, { method: "POST", headers, body: JSON.stringify(body) });
    } catch (e) {
      return { ok: false, code: "network", message: `Couldn't reach ${s.llmRoute === "byo" ? "Anthropic" : "the OrbitCosmo proxy"}: ${e.message}` };
    }

    if (!resp.ok) {
      let detail = "";
      try {
        const errBody = await resp.json();
        detail = (errBody.error && errBody.error.message) || errBody.code || "";
      } catch (_) { /* non-JSON error body */ }
      if (resp.status === 429 && detail === "cap") {
        return { ok: false, code: "cap", message: "The shared beta budget is used up for this month — switch to your own API key in settings to keep going." };
      }
      return { ok: false, code: `http-${resp.status}`, message: detail || `Request failed (${resp.status}).` };
    }

    let data;
    try {
      data = await resp.json();
    } catch (e) {
      return { ok: false, code: "parse", message: "Unreadable response." };
    }

    const spend = await recordUsage(chosenModel, data.usage || {}, now);

    if (data.stop_reason === "max_tokens") {
      return { ok: false, code: "truncated", message: "The response ran out of room — try again (smaller sessions ingest more reliably)." };
    }

    const text = data.content && data.content[0] && data.content[0].text;
    if (typeof text !== "string") return { ok: false, code: "parse", message: "Empty response." };

    if (outputSchema) {
      try {
        return { ok: true, data: JSON.parse(text), usage: data.usage, spend };
      } catch (e) {
        return { ok: false, code: "parse", message: "Malformed structured output." };
      }
    }
    return { ok: true, data: text, usage: data.usage, spend };
  }

  const api = { callClaude, getSettings, SETTINGS_DEFAULTS, PROXY_URL, MODELS, DEFAULT_MODEL, monthKey, estUsd };

  // Dual export: CommonJS for Node tests, global attach for the SW.
  if (typeof module !== "undefined" && module.exports) {
    module.exports = api;
  } else {
    Object.assign(root, api);
  }
})(typeof globalThis !== "undefined" ? globalThis : self);
