// OrbitCosmo app window — the rail beside the graph stage: recall search over
// everything you've read, your recent sessions to pick up where you left
// off, and the reading queue (pages your reading references that you never
// opened, ranked worker-side via orbit:queue). Mirrors the side panel's
// search flow (same background messages; the query is embedded in the
// service worker, vectors never reach this page). The canvas + detail sheet
// are graph.js; openPage (normal-window opening), focusReference and
// ghostTitle (graph.js), and timeAgo (search.js) are shared globals loaded
// before this script.

function esc(s) {
  return String(s == null ? "" : s).replace(/[&<>"']/g, (c) =>
    ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
}
function send(msg) {
  return new Promise((resolve) => chrome.runtime.sendMessage(msg, resolve));
}

// Plain-language phrasing for searchGraph's matchReasons (same as the panel).
const REASON_LABEL = {
  meaning: "matches by meaning",
  title: "title",
  keywords: "keywords",
  domain: "site",
  description: "description",
  "mentions it": "mentions it",
};

let railGraph = { nodes: {}, concepts: {}, sessions: {} };
let memQuery = "";
let searchSeq = 0; // monotonic guard — a stale response never overwrites a newer one
let searchTimer = null;
let pendingTimer = null;
let warmed = false;

const memqEl = document.getElementById("memq");
const resultsEl = document.getElementById("mem-results");
const pickupEl = document.getElementById("pickup");
const queueEl = document.getElementById("queue");
let queueItems = [];

function setMemStatus(text) {
  document.getElementById("memStatus").textContent = text || "";
}
function openRows(el) {
  el.querySelectorAll(".rel").forEach((b) =>
    b.addEventListener("click", () => openPage(b.dataset.url)));
}

function renderResults(items) {
  if (!items.length) {
    resultsEl.innerHTML = `<div class="rail-empty">Nothing matches yet.<br><b>OrbitCosmo only recalls pages you've read</b> — try different words.</div>`;
    return;
  }
  const now = Date.now();
  resultsEl.innerHTML = items.map(({ node: n, matchReasons }) => {
    if (!n) return "";
    // Favicon is page-supplied — only let http(s)/data:image URLs into src.
    const fav = n.favicon && /^(https?:|data:image\/)/.test(n.favicon)
      ? `<img class="fav" src="${esc(n.favicon)}" alt="" />`
      : `<span class="dot"></span>`;
    const topic = (n.conceptIds || []).length && railGraph.concepts[n.conceptIds[0]]
      ? ` · ${esc(railGraph.concepts[n.conceptIds[0]].label)}`
      : "";
    const why = (matchReasons || []).map((r) => REASON_LABEL[r] || r).join(" · ");
    return `<button class="rel" data-url="${esc(n.url)}">
      ${fav}
      <span class="meta">
        <span class="rt">${esc(n.title || n.domain || n.url)}</span>
        <span class="rd">${esc(n.domain || "")} · ${esc(timeAgo(n.lastVisit, now))}${topic}</span>
        ${why ? `<span class="why">${esc(why)}</span>` : ""}
      </span>
    </button>`;
  }).join("");
  openRows(resultsEl);
}

// Recent research threads: the last few sessions, each with its latest pages.
function renderPickup() {
  const now = Date.now();
  const sessions = Object.values(railGraph.sessions || {})
    .sort((a, b) => (b.lastSeen || 0) - (a.lastSeen || 0));
  const groups = [];
  for (const s of sessions) {
    if (groups.length >= 3) break;
    const pages = Object.values(railGraph.nodes)
      .filter((n) => n.sessionId === s.id)
      .sort((a, b) => (b.lastVisit || 0) - (a.lastVisit || 0))
      .slice(0, 3);
    if (pages.length) groups.push({ s, pages });
  }
  if (!groups.length) {
    pickupEl.innerHTML = `<div class="rail-empty">Nothing here yet.<br><b>Keep browsing</b> — OrbitCosmo connects pages as you go.</div>`;
    return;
  }
  pickupEl.innerHTML =
    `<div class="section-title">Pick up where you left off</div>` +
    groups.map(({ s, pages }) =>
      `<div class="pickup-head"><span class="s-label">${esc(s.label)}</span><span class="s-when">${esc(timeAgo(s.lastSeen, now))}</span></div>` +
      pages.map((p) => `<button class="rel" data-url="${esc(p.url)}">
        <span class="meta"><span class="rt">${esc(p.title || p.domain || p.url)}</span><span class="rd">${esc(p.domain || "")} · ${esc(timeAgo(p.lastVisit, now))}</span></span>
      </button>`).join("")
    ).join("");
  openRows(pickupEl);
}

// The reading queue: pages your reading references, never opened — ranked
// worker-side (reference count × source diversity × recency). Rows walk the
// graph to the ghost in context (focusReference, graph.js); becoming real is
// one click away via the ghost sheet's Open page.
function renderQueue() {
  if (!Object.keys(railGraph.nodes).length) { queueEl.innerHTML = ""; return; }
  const rows = queueItems.slice(0, 8);
  if (!rows.length) {
    queueEl.innerHTML =
      `<div class="section-title">Reading queue</div>
       <div class="rail-empty">Pages your reading references — but you haven't opened — will queue up here.</div>`;
    return;
  }
  queueEl.innerHTML =
    `<div class="section-title">Reading queue</div>` +
    rows.map((it) => {
      const n = it.refCount || 0;
      const m = (it.sourceDomains || []).length;
      const why = `referenced by ${n} page${n === 1 ? "" : "s"}` +
        (m >= 2 ? ` across ${m} sites` : "");
      return `<button class="rel" data-url="${esc(it.url)}">
        <span class="dot ghost"></span>
        <span class="meta">
          <span class="rt">${esc(ghostTitle(it))}</span>
          <span class="rd">${esc(it.domain || "")}</span>
          <span class="why">${esc(why)}</span>
        </span>
      </button>`;
    }).join("");
  queueEl.querySelectorAll(".rel").forEach((b) =>
    b.addEventListener("click", () => focusReference(b.dataset.url)));
}

function refreshQueue() {
  send({ type: "orbit:queue" }).then((resp) => {
    queueItems = resp && resp.ok ? resp.items : [];
    renderQueue();
  });
}

function renderRail() {
  const searching = !!memQuery;
  resultsEl.hidden = !searching;
  pickupEl.hidden = searching;
  wikiListEl.hidden = searching;
  queueEl.hidden = searching;
  if (searching) return; // results render when the response lands
  resultsEl.innerHTML = "";
  renderPickup();
  renderQueue();
}

function runSearch(query) {
  const seq = ++searchSeq;
  clearTimeout(pendingTimer);
  // Only surface the cold-start hint if the answer is actually slow in coming.
  pendingTimer = setTimeout(() => setMemStatus("warming up the local model…"), 400);
  send({ type: "orbit:search", query }).then((resp) => {
    if (seq !== searchSeq) return; // a newer query superseded this one
    clearTimeout(pendingTimer);
    if (!resp || !resp.ok) {
      setMemStatus("Search hiccuped — try again.");
      return;
    }
    setMemStatus("");
    if (!memQuery) return; // cleared while in flight — idle view already rendered
    renderResults(resp.results.filter((r) => r.node));
  });
}

memqEl.addEventListener("focus", () => {
  // Absorb the offscreen model's cold start before the first query lands.
  if (warmed) return;
  warmed = true;
  send({ type: "orbit:warmup" });
});
memqEl.addEventListener("input", () => {
  const q = memqEl.value.trim();
  if (q === memQuery) return;
  memQuery = q;
  clearTimeout(searchTimer);
  renderRail(); // swap between results and idle sections immediately
  if (!q) { searchSeq++; clearTimeout(pendingTimer); setMemStatus(""); return; }
  searchTimer = setTimeout(() => runSearch(q), 250);
});
memqEl.addEventListener("keydown", (e) => {
  if (e.key === "Escape") {
    memqEl.value = "";
    memqEl.dispatchEvent(new Event("input"));
    e.stopPropagation(); // clear the search without also closing the sheet
  }
});

function syncThemeBtn() {
  const b = document.getElementById("theme-btn");
  if (b) b.title = OrbitTheme.get() === "dark"
    ? "Switch to the light paper theme" : "Switch to the dark theme";
}
document.getElementById("theme-btn")?.addEventListener("click", () => OrbitTheme.toggle());
OrbitTheme.onChange(syncThemeBtn);
syncThemeBtn();

function refreshRail() {
  if (!chrome?.runtime?.sendMessage) return; // graph.js shows the open-from-extension hint
  send({ type: "orbit:getGraph" }).then((g) => {
    railGraph = g && g.nodes ? g : { nodes: {}, concepts: {}, sessions: {} };
    renderRail();
  });
  refreshQueue();
}

// graph.js reloads the canvas on the same signal; each surface refreshes itself.
if (chrome?.storage?.onChanged) {
  chrome.storage.onChanged.addListener((changes, area) => {
    if (area === "local" && changes.orbit_graph_rev) refreshRail();
    if (area === "local" && changes.orbit_wiki_rev) { renderWikiSection(); refreshWikiStatus(); }
  });
}

// --- wiki: the ⚙ settings pane + the rail's Wiki section ----------------------
// Settings live in chrome.storage.local under orbit_settings — the worker
// (claude.js) reads the same key, so this page writes the whole object back
// on every change. Secrets never leave this profile's local storage.
const SETTINGS_KEY = "orbit_settings";
const SETTINGS_DEFAULTS = {
  wikiEnabled: false, llmRoute: "proxy", proxyToken: "", anthropicKey: "",
  model: "claude-haiku-4-5", monthlyCapUsd: 3,
};
let settings = Object.assign({}, SETTINGS_DEFAULTS);
let ingesting = false;

const settingsBtn = document.getElementById("settings-btn");
const settingsEl = document.getElementById("settings");
const setWikiOnEl = document.getElementById("setWikiOn");
const setSecretEl = document.getElementById("setSecret");
const setModelEl = document.getElementById("setModel");
const setIngestBtn = document.getElementById("setIngest");
const setIngestStatusEl = document.getElementById("setIngestStatus");
const setWikiStatusEl = document.getElementById("setWikiStatus");
const setRouteEls = Array.from(document.querySelectorAll('input[name="setRoute"]'));
const wikiListEl = document.getElementById("wiki");

function saveSettings(patch) {
  Object.assign(settings, patch);
  chrome?.storage?.local?.set?.({ [SETTINGS_KEY]: settings });
}

// The secret input holds whichever credential the active route uses.
function syncSettingsUI() {
  setWikiOnEl.checked = !!settings.wikiEnabled;
  setRouteEls.forEach((r) => { r.checked = r.value === settings.llmRoute; });
  const byo = settings.llmRoute === "byo";
  setSecretEl.placeholder = byo ? "Your Anthropic API key" : "OrbitCosmo beta token";
  setSecretEl.value = byo ? settings.anthropicKey : settings.proxyToken;
  setModelEl.value = settings.model;
}

function loadSettings() {
  chrome?.storage?.local?.get?.(SETTINGS_KEY, (r) => {
    settings = Object.assign({}, SETTINGS_DEFAULTS, r?.[SETTINGS_KEY] || {});
    syncSettingsUI();
  });
}

settingsBtn.addEventListener("click", () => {
  settingsEl.hidden = !settingsEl.hidden;
  settingsBtn.setAttribute("aria-expanded", String(!settingsEl.hidden));
  if (!settingsEl.hidden) refreshWikiStatus();
});
setWikiOnEl.addEventListener("change", () => {
  saveSettings({ wikiEnabled: setWikiOnEl.checked });
  refreshWikiStatus();
});
setRouteEls.forEach((r) => r.addEventListener("change", () => {
  if (!r.checked) return;
  saveSettings({ llmRoute: r.value });
  syncSettingsUI();
  refreshWikiStatus();
}));
setSecretEl.addEventListener("change", () => {
  saveSettings(settings.llmRoute === "byo"
    ? { anthropicKey: setSecretEl.value.trim() }
    : { proxyToken: setSecretEl.value.trim() });
  refreshWikiStatus();
});
setModelEl.addEventListener("change", () => saveSettings({ model: setModelEl.value }));

function refreshWikiStatus() {
  if (!chrome?.runtime?.sendMessage) return;
  send({ type: "orbit:wikiStatus" }).then((st) => {
    if (!st) return;
    if (st.lastError) { setWikiStatusEl.textContent = st.lastError.message; return; }
    if (!st.enabled) { setWikiStatusEl.textContent = "The wiki is off."; return; }
    if (!st.configured) {
      setWikiStatusEl.textContent = st.route === "byo"
        ? "Add your Anthropic key to finish setup."
        : "Add your beta token to finish setup.";
      return;
    }
    const spent = st.spend ? st.spend.estUsd : 0;
    setWikiStatusEl.textContent =
      `${st.pageCount} page${st.pageCount === 1 ? "" : "s"} · $${spent.toFixed(2)} of $${st.capUsd} this month`;
  });
}

// Ingest runs 10–30s in the worker; the button carries the busy state.
setIngestBtn.addEventListener("click", () => {
  if (ingesting || !chrome?.runtime?.sendMessage) return;
  ingesting = true;
  setIngestBtn.disabled = true;
  setIngestBtn.classList.add("busy");
  setIngestBtn.textContent = "Updating…";
  setIngestStatusEl.textContent = "";
  send({ type: "orbit:wikiIngest" }).then((resp) => {
    ingesting = false;
    setIngestBtn.disabled = false;
    setIngestBtn.classList.remove("busy");
    setIngestBtn.textContent = "Update wiki now";
    setIngestStatusEl.textContent =
      !resp ? "No reply — try again."
      : resp.ok && resp.skipped ? resp.message
      : resp.ok ? `${resp.pages.length} page${resp.pages.length === 1 ? "" : "s"} updated`
      : resp.message;
    refreshWikiStatus();
    renderWikiSection();
  });
});

// Export: every page (index/log/schema included — they're part of the wiki
// on disk) as slug.md entries in one store-only zip (wiki-export.js).
document.getElementById("setExport").addEventListener("click", () => {
  if (!chrome?.runtime?.sendMessage) return;
  send({ type: "orbit:wikiExport" }).then((pages) => {
    if (!Array.isArray(pages) || !pages.length) {
      setIngestStatusEl.textContent = "Nothing to export yet.";
      return;
    }
    const zip = wikiToZip(pages);
    const url = URL.createObjectURL(new Blob([zip], { type: "application/zip" }));
    const a = document.createElement("a");
    a.href = url;
    a.download = "orbitcosmo-wiki.zip";
    a.click();
    URL.revokeObjectURL(url);
  });
});

// The rail's Wiki section: readable pages only — index/log/schema are
// plumbing. Titles are LLM-written, so rows are built with createElement +
// textContent, never innerHTML. Clicking opens the page in the sheet
// (showWikiPage, graph.js — loaded before this script).
const WIKI_INTERNAL_KINDS = ["index", "log", "schema"];
function renderWikiSection() {
  if (!chrome?.runtime?.sendMessage) return;
  send({ type: "orbit:wikiList" }).then((pages) => {
    const shown = Array.isArray(pages)
      ? pages.filter((p) => !WIKI_INTERNAL_KINDS.includes(p.kind))
      : [];
    wikiListEl.textContent = "";
    if (!shown.length) return;
    const now = Date.now();
    const head = document.createElement("div");
    head.className = "section-title";
    head.textContent = "Wiki";
    wikiListEl.appendChild(head);
    for (const p of shown) {
      const b = document.createElement("button");
      b.className = "rel";
      const meta = document.createElement("span");
      meta.className = "meta";
      const rt = document.createElement("span");
      rt.className = "rt";
      rt.textContent = p.title || p.slug;
      const rd = document.createElement("span");
      rd.className = "rd";
      rd.textContent = `${p.kind} · ${timeAgo(p.updatedTs, now)}`;
      meta.appendChild(rt);
      meta.appendChild(rd);
      b.appendChild(meta);
      b.addEventListener("click", () => showWikiPage(p.slug));
      wikiListEl.appendChild(b);
    }
  });
}

loadSettings();
refreshWikiStatus();
renderWikiSection();
refreshRail();
