// OrbitCosmo side panel — your research memory, one view. Recall search over
// everything you've read, context for the page you're on, and your recent
// sessions to pick up where you left off. Reads the live graph from the
// service worker; search queries are ranked there too (embeddings never reach
// this page — see pipeline.js handleSearch). search.js is loaded alongside
// for timeAgo.

const TRACKING = ["utm_source","utm_medium","utm_campaign","utm_term","utm_content","gclid","fbclid","ref","ref_src"];
// Plain-language relationship labels for the connections list.
const TYPE_LABEL = {
  navigation: "You clicked through",
  related: "Related — same subject",
  manual: "You linked it yourself",
  reference: "One cites the other",
};

let lastGraph = { nodes: {}, edges: {}, concepts: {}, sessions: {}, currentSessionId: null };
let lastNode = null;

// --- helpers ---------------------------------------------------------------
function esc(s) {
  return String(s == null ? "" : s).replace(/[&<>"']/g, (c) =>
    ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
}
function normalizeUrl(url) {
  try {
    const u = new URL(url);
    u.hash = "";
    TRACKING.forEach((p) => u.searchParams.delete(p));
    return u.toString();
  } catch { return url; }
}
function send(msg) {
  return new Promise((resolve) => chrome.runtime.sendMessage(msg, resolve));
}
function getGraph() {
  return send({ type: "orbit:getGraph" }).then((g) =>
    g && g.nodes ? g : { nodes: {}, edges: {}, concepts: {}, sessions: {}, currentSessionId: null });
}
function activeTab() {
  return new Promise((resolve) =>
    chrome.tabs.query({ active: true, currentWindow: true }, (t) => resolve(t && t[0])));
}
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";
}

function findNode(graph, rawUrl) {
  if (!rawUrl) return null;
  const norm = normalizeUrl(rawUrl);
  if (graph.nodes[norm]) return graph.nodes[norm];
  // Fallback: match on origin + pathname (ignore query/trailing slash).
  let key;
  try { const u = new URL(norm); key = u.origin + u.pathname.replace(/\/$/, ""); }
  catch { return null; }
  for (const k of Object.keys(graph.nodes)) {
    try { const u = new URL(k); if (u.origin + u.pathname.replace(/\/$/, "") === key) return graph.nodes[k]; }
    catch { /* skip */ }
  }
  return null;
}

function openRows(el) {
  el.querySelectorAll(".rel").forEach((b) =>
    b.addEventListener("click", () => chrome.tabs.create({ url: b.dataset.url, active: true })));
}

// --- "on this page": current card + connections ------------------------------
function renderConnections(graph, node) {
  const curEl = document.getElementById("current");
  const relEl = document.getElementById("related");
  if (!node) {
    curEl.innerHTML = "";
    relEl.innerHTML = `<div class="empty">This page isn't in your graph yet.<br><b>Keep browsing</b> — OrbitCosmo connects pages as you go.</div>`;
    return;
  }
  // The transcript affordance (quiet, nothing clickable): the node carries
  // transcriptCaptured once a YouTube transcript-panel open was captured.
  curEl.innerHTML = `<div class="card">
    <div class="eyebrow">You're on</div>
    <div class="title">${esc(node.title || node.url)}</div>
    <div class="domain">${esc(node.domain || "")}</div>
    ${node.transcriptCaptured ? `<div class="note">Transcript captured — it's part of this page's memory now.</div>` : ""}
  </div>`;

  // Same helper the app window's ego view is built on (ego.js): the panel's
  // "Connects to" list IS the hop-1 ego neighborhood. Deliberate change from
  // the old list: the header count is now capped with the rows (max 30), so
  // the number always matches what's shown. maxGhosts: 0 — the queue lives
  // in the app window; the panel stays real pages only.
  const nb = getEgoNeighborhood(graph, node.url, { hops: 1, maxNodes: 31, maxGhosts: 0 }).nodes;
  if (!nb.length) {
    relEl.innerHTML = `<div class="empty">No connections yet for this page.<br>As you visit related pages they'll show up here.</div>`;
    return;
  }
  relEl.innerHTML =
    `<div class="section-title">Connects to <small>${nb.length}</small></div>` +
    nb.map((n) => {
      const other = graph.nodes[n.url];
      return `<button class="rel" data-url="${esc(n.url)}">
      <span class="dot ${n.type}"></span>
      <span class="meta"><span class="rt">${esc(other.title || other.domain || n.url)}</span><span class="rd">${esc(other.domain || "")}</span></span>
      <span class="rtype">${esc(TYPE_LABEL[n.type] || n.type)}</span>
    </button>`;
    }).join("");
  openRows(relEl);
}

// --- "same sitting": temporal recall for the current page --------------------
let sittingSeq = 0; // monotonic guard — a stale response never overwrites a newer one

// The wire (orbit:sitting) carries only URLs + numbers; titles and favicons
// come from the graph the panel already holds. The block is additive — when
// there's nothing to say it simply isn't there (no empty-state).
function renderSitting(items) {
  const el = document.getElementById("sitting");
  const rows = (items || []).filter((it) => lastGraph.nodes[it.url]);
  if (!rows.length) { el.innerHTML = ""; return; }
  const now = Date.now();
  el.innerHTML =
    `<div class="section-title">Same sitting <small>${rows.length}</small></div>` +
    rows.map((it) => {
      const n = lastGraph.nodes[it.url];
      // 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>`;
      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(String(it.count))}× together · ${esc(timeAgo(it.lastSeen, now))}</span></span>
    </button>`;
    }).join("");
  openRows(el);
}

function refreshSitting() {
  const seq = ++sittingSeq;
  if (!lastNode) { renderSitting([]); return; }
  send({ type: "orbit:sitting", url: lastNode.url }).then((resp) => {
    if (seq !== sittingSeq) return; // a newer page superseded this one
    renderSitting(resp && resp.ok ? resp.items : []);
  });
}

// --- recall search ------------------------------------------------------------
// Human phrasing for searchGraph's matchReasons.
const REASON_LABEL = {
  meaning: "matches by meaning",
  title: "title",
  keywords: "keywords",
  domain: "site",
  description: "description",
  "mentions it": "mentions it",
};

let memQuery = "";
let searchSeq = 0; // monotonic guard — a stale response never overwrites a newer one
let searchTimer = null;
let pendingTimer = null;
let warmed = false;

function setMemStatus(text) {
  document.getElementById("memStatus").textContent = text || "";
}

function renderResults(items) {
  const el = document.getElementById("mem-results");
  if (!items.length) {
    el.innerHTML = `<div class="empty">Nothing matches yet.<br><b>OrbitCosmo only recalls pages you've read</b> — try different words.</div>`;
    return;
  }
  const now = Date.now();
  el.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 && lastGraph.concepts[n.conceptIds[0]]
      ? ` · ${esc(lastGraph.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(el);
}

// Recent research threads: the last few sessions, each with its latest pages.
function renderPickup() {
  const el = document.getElementById("pickup");
  const now = Date.now();
  const sessions = Object.values(lastGraph.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(lastGraph.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) { el.innerHTML = ""; return; }
  el.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(el);
}

function renderMemory() {
  const results = document.getElementById("mem-results");
  const searching = !!memQuery;
  results.hidden = !searching;
  document.getElementById("current").hidden = searching;
  document.getElementById("related").hidden = searching;
  document.getElementById("sitting").hidden = searching;
  document.getElementById("pickup").hidden = searching;
  if (searching) return; // results render when the response lands
  results.innerHTML = "";
  renderConnections(lastGraph, lastNode);
  renderPickup();
}

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));
  });
}

// --- orchestration ---------------------------------------------------------
async function refresh() {
  const [graph, tab] = await Promise.all([getGraph(), activeTab()]);
  lastGraph = graph;
  lastNode = findNode(graph, tab && tab.url);
  renderMemory();
  refreshSitting();
}

// --- events ----------------------------------------------------------------
const memqEl = document.getElementById("memq");
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);
  renderMemory(); // 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"));
  }
});

document.getElementById("theme-btn")?.addEventListener("click", () => OrbitTheme.toggle());
OrbitTheme.onChange(syncThemeBtn);
syncThemeBtn();

// Open the OrbitCosmo app window — the whole memory (search, sessions, graph) in
// an app-style popup window. The background owns the window (one instance,
// focused if already open, bounds remembered).
document.getElementById("expand").addEventListener("click", () =>
  send({ type: "orbit:openApp" }));

chrome.tabs.onActivated.addListener(refresh);
chrome.tabs.onUpdated.addListener((id, info) => { if (info.status === "complete" || info.url) refresh(); });
chrome.storage.onChanged.addListener((changes, area) => { if (area === "local" && changes.orbit_graph_rev) refresh(); });

refresh();
