// Kronaxis Toolbar — background service worker.
// Flow: content.js extracts the page text and asks us to score the page. We get
// the server-side descriptive signals (authority, topicality, and any page axes
// already in the corpus/cache), and when the page has no precomputed character we
// run the on-device student (in the offscreen document) on the visitor's own
// machine — their resources, not ours — then, if the visitor consents, contribute
// the computed axes back so everyone benefits next time (the flywheel).
//
// Consent model: everything defaults ON at install (the install + onboarding IS
// the informed consent). Each feature can be switched OFF, which disables it:
//   toolbar      -> show the strip + compute on-device character
//   visitStream  -> contribute the computed page axes + visit back to us

const API = "https://toolbar.kronaxis.com";  // public HTTPS via VPS Apache -> WG -> DL580:8113
const DEFAULTS = { toolbar: true, visitStream: true, onboarded: false, kxKey: "", kxTier: "free" };

async function cfg() { return new Promise(r => chrome.storage.local.get(DEFAULTS, r)); }
function pld(u) { try { return new URL(u).hostname.replace(/^www\./, ""); } catch (e) { return ""; } }

// Per-user key: self-issued at first use, sent on every request and used to sign
// responses, rate limit, and (if a leak is detected) revoke. Stored locally.
async function getKey() {
  const c = await cfg();
  if (c.kxKey) return c.kxKey;
  try {
    const r = await fetch(`${API}/key/register`, { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" });
    const j = await r.json();
    if (j && j.key) { await chrome.storage.local.set({ kxKey: j.key, kxTier: j.tier || "free" }); return j.key; }
  } catch (e) {}
  return "";
}
function authHeaders(key) { return key ? { "X-KX-Key": key } : {}; }

async function scoreFor(url) {
  const d = pld(url);
  if (!d) return null;
  const r = await fetch(`${API}/score?domain=${encodeURIComponent(d)}&url=${encodeURIComponent(url)}`);
  return r.ok ? r.json() : null;
}
function postPageScore(url, axes) {
  fetch(`${API}/page-score`, {
    method: "POST", headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ url, axes })
  }).catch(() => {});
}
// Send a visited page + its locally computed DW character, tied to the user's key (hash id).
// This is the unifying primitive for all three profile modes: the server aggregates a user's
// per-page DW into their DYNAMICS-8.
async function postVisit(url, axes) {
  const key = await getKey();
  fetch(`${API}/visit`, {
    method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, authHeaders(key)),
    body: JSON.stringify({ url, axes, ts: Date.now() })
  }).catch(() => {});
}

// Pull the user's browsing history (most visited first).
async function historyUrls(limit = 1000) {
  const items = await chrome.history.search({ text: "", maxResults: limit, startTime: 0 });
  return items.filter(i => /^https?:/.test(i.url))
    .map(i => ({ url: i.url, visits: i.visitCount || 1 }))
    .sort((a, b) => b.visits - a.visits);
}
// Fetch a page and reduce it to plain text (background fetch; needs broad host permission).
async function fetchText(url) {
  try {
    const r = await fetch(url, { credentials: "omit", redirect: "follow" });
    const ct = r.headers.get("content-type") || "";
    if (!/text|html/.test(ct)) return "";
    const html = await r.text();
    return html.replace(/<(script|style|nav|footer|header|svg)[^>]*>[\s\S]*?<\/\1>/gi, " ")
      .replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 2000);
  } catch (e) { return ""; }
}

// Build the user's profile from history. mode: 'server' | 'local' | (live-accrual needs no build).
async function buildHistory(mode, max = 60) {
  const urls = (await historyUrls(1500)).slice(0, max);
  if (!urls.length) return { ok: false, reason: "no history" };
  if (mode === "server") {
    const key = await getKey();
    const r = await fetch(`${API}/profile-from-history`, {
      method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, authHeaders(key)),
      body: JSON.stringify({ urls })
    });
    return { ok: r.ok, mode, body: await r.json().catch(() => null) };
  }
  // local: fetch + score each page on-device, send the page DW (tied to the key) via /visit
  let scored = 0;
  for (const it of urls) {
    const text = await fetchText(it.url);
    if (text.length < 120) continue;
    const r = await embedOnDevice(text);
    if (r && r.ok && r.axes) { await postVisit(it.url, r.axes); scored++; }
  }
  return { ok: true, mode, scored, total: urls.length };
}
async function getProfile() {
  const key = await getKey();
  const r = await fetch(`${API}/profile`, { headers: authHeaders(key) });
  return r.ok ? r.json() : null;
}

// ---- offscreen document (hosts the WASM model) ----
let creatingOffscreen = null;
async function ensureOffscreen() {
  const has = await chrome.offscreen.hasDocument();
  if (has) return;
  if (!creatingOffscreen) {
    creatingOffscreen = chrome.offscreen.createDocument({
      url: "offscreen.html",
      reasons: ["WORKERS"],
      justification: "Run the on-device DYNAMICS-WEB student model (WebAssembly) on page text."
    }).finally(() => { creatingOffscreen = null; });
  }
  await creatingOffscreen;
}
async function embedOnDevice(text) {
  await ensureOffscreen();
  return new Promise(res => {
    chrome.runtime.sendMessage({ target: "offscreen", type: "embed", text }, r => {
      if (chrome.runtime.lastError) return res(null);
      res(r || null);
    });
  });
}

const tabScore = {};   // last score per tab for the popup

async function handleScore(tab, msg) {
  const c = await cfg();
  if (!c.toolbar) return null;
  const url = msg.url || (tab && tab.url);
  const s = (await scoreFor(url).catch(() => null)) || { domain: pld(url) };
  // On-device page character only when the page is not already scored server side.
  if (!s.page_dynamics_web && msg.text && msg.text.length > 80) {
    const text = (msg.title ? msg.title.slice(0, 200) + "\n\n" : "") + msg.text.slice(0, 2000);
    const r = await embedOnDevice(text).catch(() => null);
    if (r && r.ok && r.axes) {
      s.page_dynamics_web = r.axes;
      s.page_in_corpus = true;
      s.page_status = "on-device";
    }
  }
  // Log every visited page with its DW (from any source) tied to the user — live-accrual.
  if (c.visitStream && s.page_dynamics_web) postVisit(url, s.page_dynamics_web);
  if (tab && tab.id != null) {
    tabScore[tab.id] = s;
    const top = s.ttf && s.ttf[0] && s.ttf_confident ? "•" : "";
    chrome.action.setBadgeText({ tabId: tab.id, text: top });
    chrome.action.setBadgeBackgroundColor({ tabId: tab.id, color: "#6d28d9" });
  }
  return s;
}

// ---- freemium: audiences, fit (free diagnosis), cure (premium) ----
async function kxState() {
  const key = await getKey();
  let tier = "free", audiences = [];
  try {
    const r = await fetch(`${API}/audiences`, { headers: authHeaders(key) });
    const j = await r.json();
    tier = j.tier || "free"; audiences = j.audiences || [];
    await chrome.storage.local.set({ kxTier: tier });
  } catch (e) {}
  return { key: !!key, tier, audiences };
}
async function kxFit(url, audience) {
  const key = await getKey();
  const r = await fetch(`${API}/fit?url=${encodeURIComponent(url)}&audience=${encodeURIComponent(audience)}`, { headers: authHeaders(key) });
  return { status: r.status, body: await r.json().catch(() => null) };
}
async function kxCure(url, audience, text) {
  const key = await getKey();
  const r = await fetch(`${API}/cure`, {
    method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, authHeaders(key)),
    body: JSON.stringify({ url, audience, text: (text || "").slice(0, 1800) })
  });
  return { status: r.status, body: await r.json().catch(() => null) };
}

// Register interest in a language (UK English only today) — gauges demand for what to build next.
async function kxLangRequest(lang) {
  const key = await getKey();
  const r = await fetch(`${API}/lang-request`, {
    method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, authHeaders(key)),
    body: JSON.stringify({ lang })
  });
  return r.ok ? r.json() : null;
}
// Save a custom reader (defined with the sliders) server side so it scores like a built-in one.
async function kxSaveAudience(name, target) {
  const key = await getKey();
  const r = await fetch(`${API}/audience/save`, {
    method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, authHeaders(key)),
    body: JSON.stringify({ name, target })
  });
  return { status: r.status, body: await r.json().catch(() => null) };
}
// Score an arbitrary passage on-device (a single paragraph) — content stays local.
async function kxScoreText(text) {
  const r = await embedOnDevice((text || "").slice(0, 2000)).catch(() => null);
  return (r && r.ok && r.axes) ? r.axes : null;
}
// Direction for a paragraph from its locally-computed axes — only the 8 numbers leave, no text.
async function kxFitAxes(axes, audience) {
  const r = await fetch(`${API}/fit-axes`, {
    method: "POST", headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ axes, audience })
  });
  return r.ok ? r.json() : null;
}
async function kxAdvise(url, audience, text) {
  const key = await getKey();
  const r = await fetch(`${API}/advise`, {
    method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, authHeaders(key)),
    body: JSON.stringify({ url, audience, text: (text || "").slice(0, 1200) })
  });
  return { status: r.status, body: await r.json().catch(() => null) };
}
async function kxShape(url, target, text) {
  const key = await getKey();
  const r = await fetch(`${API}/shape`, {
    method: "POST", headers: Object.assign({ "Content-Type": "application/json" }, authHeaders(key)),
    body: JSON.stringify({ url, target, text: (text || "").slice(0, 1800) })
  });
  return { status: r.status, body: await r.json().catch(() => null) };
}

chrome.runtime.onMessage.addListener((msg, sender, send) => {
  if (!msg || msg.target === "offscreen") return false;       // offscreen handles its own
  if (msg.type === "kxShape") { kxShape(msg.url, msg.target, msg.text).then(send).catch(() => send(null)); return true; }
  if (msg.type === "scorePageWithText") {
    handleScore(sender.tab, msg).then(s => send(s)).catch(() => send(null));
    return true; // async
  }
  if (msg.type === "getScore") { send(tabScore[msg.tabId] || null); return false; }
  if (msg.type === "kxState") { kxState().then(send).catch(() => send({ tier: "free", audiences: [] })); return true; }
  if (msg.type === "kxFit") { kxFit(msg.url, msg.audience).then(send).catch(() => send(null)); return true; }
  if (msg.type === "kxCure") { kxCure(msg.url, msg.audience, msg.text).then(send).catch(() => send(null)); return true; }
  if (msg.type === "kxAdvise") { kxAdvise(msg.url, msg.audience, msg.text).then(send).catch(() => send(null)); return true; }
  if (msg.type === "kxScoreText") { kxScoreText(msg.text).then(send).catch(() => send(null)); return true; }
  if (msg.type === "kxFitAxes") { kxFitAxes(msg.axes, msg.audience).then(send).catch(() => send(null)); return true; }
  if (msg.type === "kxLangRequest") { kxLangRequest(msg.lang).then(send).catch(() => send(null)); return true; }
  if (msg.type === "kxSaveAudience") { kxSaveAudience(msg.name, msg.target).then(send).catch(() => send(null)); return true; }
  if (msg.type === "kxProfile") { getProfile().then(send).catch(() => send(null)); return true; }
  if (msg.type === "kxBuildHistory") { buildHistory(msg.mode).then(send).catch(() => send({ ok: false })); return true; }
  return false;
});

chrome.runtime.onInstalled.addListener(() => {
  getKey();   // self-issue the per-user key up front
  chrome.storage.local.get(DEFAULTS, c => { if (!c.onboarded) chrome.runtime.openOptionsPage(); });
});
