// Pull in the classifier so every intercepted item gets a verdict
// (meaningful / noise / unknown) the moment it lands, and the verdict is
// echoed to the service-worker console as a one-line log.
try {
  importScripts("classify.js");
} catch (e) {
  console.warn("[CASINO] classify.js failed to load:", e);
}

// Pull in the hand pipeline so the service worker can run autoflush on a
// timer even when the History tab is closed. Order matters: sessionize
// + serialize define globals that flush.js reads at module-load time.
try {
  // `users.js` exposes CasinoUsers.buildIndex(messages) and is needed
  // by flush.js so the flush container can carry a `userId -> username`
  // mapping for the server-side per-player tree on statisticasino.
  importScripts("tableize.js", "serialize.js", "users.js", "flush.js");
} catch (e) {
  console.warn("[CASINO] flush pipeline failed to load:", e);
}
const Flush = (typeof self !== "undefined" && self.CasinoFlush) || null;

// The toolbar action no longer opens a popup; clicking the icon opens the
// History page as a full browser tab. If a History tab is already open we
// focus it instead of stacking duplicates.
chrome.action.onClicked.addListener(async () => {
  const url = chrome.runtime.getURL("history.html");
  try {
    const existing = await chrome.tabs.query({ url });
    if (existing && existing.length > 0) {
      const t = existing[0];
      await chrome.tabs.update(t.id, { active: true });
      if (t.windowId != null) await chrome.windows.update(t.windowId, { focused: true });
      return;
    }
  } catch (_) { /* fall through to create */ }
  await chrome.tabs.create({ url });
});

const CASINO_FILTER = { urls: ["https://*.casino.org/*"] };
const NETWORK_KEY = "network";
const PAGEDATA_KEY = "pageData";
const MESSAGES_KEY = "messages";
const SETTINGS_KEY = "settings";
const MAX_NETWORK = 500;
const MAX_PAGEDATA = 200;
const MAX_MESSAGES = 2000;
const FLUSH_MS = 300;

// Capture gate. Mirrors `settings.captureEnabled` in chrome.storage.local;
// when false we still log verdicts to the console for debugging but skip the
// pending queue so nothing hits storage. Defaults to true until we hear back
// from storage on startup.
let captureEnabled = true;
chrome.storage.local.get([SETTINGS_KEY], (data) => {
  const s = data && data[SETTINGS_KEY];
  if (s && typeof s === "object") {
    if (typeof s.captureEnabled === "boolean") captureEnabled = s.captureEnabled;
  }
});
chrome.storage.onChanged.addListener((changes, area) => {
  if (area !== "local") return;
  if (!changes[SETTINGS_KEY]) return;
  const next = changes[SETTINGS_KEY].newValue;
  if (next && typeof next === "object") {
    if (typeof next.captureEnabled === "boolean") captureEnabled = next.captureEnabled;
  }
});

const classifier = (typeof self !== "undefined" && self.CasinoClassify) || null;

// Rolling totals so the popup status bar can show a live meaningful/noise
// breakdown without re-scanning the whole stored array.
const totals = {
  messages: { meaningful: 0, noise: 0 },
  network:  { meaningful: 0, noise: 0, unknown: 0 }
};

function labelAndLog(scope, entry) {
  if (!classifier) return null;
  const verdict = scope === "network"
    ? classifier.classifyNetwork(entry)
    : classifier.classifyMessage(entry);
  entry._verdict = verdict;
  try {
    const line = classifier.formatLogLine(entry, verdict);
    if (verdict.verdict === "meaningful") console.log("%c" + line, "color:#4ade80");
    else if (verdict.verdict === "noise")  console.debug("%c" + line, "color:#8a93a6");
    else                                    console.log("%c" + line, "color:#fbbf24");
  } catch (_) { /* logging is best-effort */ }
  if (totals[scope] && verdict && totals[scope][verdict.verdict] != null) {
    totals[scope][verdict.verdict]++;
  }
  return verdict;
}

// Service workers can be torn down between events; in-memory queues are
// coalesced and flushed to chrome.storage.local on a short timer so we
// don't thrash storage on bursty network or socket traffic.
const pending = { [NETWORK_KEY]: [], [MESSAGES_KEY]: [] };
const caps = { [NETWORK_KEY]: MAX_NETWORK, [MESSAGES_KEY]: MAX_MESSAGES };
let flushScheduled = false;
let counter = 0;
function nextId() { counter = (counter + 1) % 1e9; return `${Date.now().toString(36)}-${counter.toString(36)}`; }

function scheduleFlush() {
  if (flushScheduled) return;
  flushScheduled = true;
  setTimeout(flush, FLUSH_MS);
}

async function flush() {
  flushScheduled = false;
  const keys = Object.keys(pending).filter((k) => pending[k].length > 0);
  if (keys.length === 0) return;
  const batches = {};
  for (const k of keys) {
    batches[k] = pending[k];
    pending[k] = [];
  }
  const existing = await chrome.storage.local.get(keys);
  const writes = {};
  for (const k of keys) {
    const merged = (existing[k] || []).concat(batches[k]);
    const trimmed = merged.length > caps[k] ? merged.slice(-caps[k]) : merged;
    writes[k] = trimmed;
  }
  await chrome.storage.local.set(writes);
}

// ---------------------------------------------------------- webRequest

chrome.webRequest.onCompleted.addListener((d) => {
  if (!captureEnabled) return;
  const entry = {
    id: `${d.requestId}-${d.timeStamp}`,
    url: d.url,
    method: d.method,
    type: d.type,
    status: d.statusCode,
    fromCache: d.fromCache,
    ip: d.ip || null,
    ts: d.timeStamp,
    ok: d.statusCode >= 200 && d.statusCode < 400
  };
  labelAndLog("network", entry);
  pending[NETWORK_KEY].push(entry);
  scheduleFlush();
}, CASINO_FILTER);

chrome.webRequest.onErrorOccurred.addListener((d) => {
  if (!captureEnabled) return;
  const entry = {
    id: `${d.requestId}-${d.timeStamp}`,
    url: d.url,
    method: d.method,
    type: d.type,
    status: 0,
    error: d.error,
    ts: d.timeStamp,
    ok: false
  };
  labelAndLog("network", entry);
  pending[NETWORK_KEY].push(entry);
  scheduleFlush();
}, CASINO_FILTER);

// ------------------------------------------------------- runtime msgs

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (!msg || typeof msg !== "object") return;

  if (msg.kind === "pageData") {
    if (!captureEnabled) {
      sendResponse({ ok: true, dropped: true, reason: "capture-disabled" });
      return false;
    }
    handlePageData(msg, sender).then(
      (r) => sendResponse({ ok: true, ...r }),
      (e) => sendResponse({ ok: false, error: String(e) })
    );
    return true;
  }

  if (msg.kind === "captured") {
    if (!captureEnabled) {
      sendResponse({ ok: true, dropped: true, reason: "capture-disabled" });
      return false;
    }
    const entry = {
      id: nextId(),
      tabId: sender?.tab?.id ?? null,
      frameId: sender?.frameId ?? null,
      pageUrl: msg.pageUrl || sender?.tab?.url || null,
      pageTitle: msg.pageTitle || null,
      receivedTs: Date.now(),
      ...(msg.entry || {})
    };
    const verdict = labelAndLog("messages", entry);
    // Only `meaningful` entries earn a slot in the 2000-message cap:
    //   - `unknown`: classifier couldn't tell what it is — no usable signal.
    //   - `noise`:   classifier *can* tell, and it's session chatter
    //                (tick / extendedConnections / setIntent / ping / etc.)
    //                or a low-signal HTTP endpoint (iterableProxy, friends,
    //                hidden_avatars, lobby RSC dumps). See DATA_FORMAT.md §4.
    // Dropped entries are still counted in `totals` so the popup can show
    // the filter rate, but they don't consume storage.
    if (verdict && (verdict.verdict === "unknown" || verdict.verdict === "noise")) {
      sendResponse({ ok: true, verdict, dropped: true });
      return false;
    }
    pending[MESSAGES_KEY].push(entry);
    scheduleFlush();
    sendResponse({ ok: true, verdict: entry._verdict });
    return false;
  }

  if (msg.kind === "getTotals") {
    sendResponse({ ok: true, totals });
    return false;
  }
});

async function handlePageData(msg, sender) {
  const pageUrl = sender?.tab?.url || msg.url || null;
  const tabId = sender?.tab?.id ?? null;
  const entry = {
    tabId,
    url: pageUrl,
    title: msg.title || null,
    rows: Array.isArray(msg.rows) ? msg.rows : [],
    ts: Date.now()
  };
  const { [PAGEDATA_KEY]: existing = [] } = await chrome.storage.local.get(PAGEDATA_KEY);
  // Replace any prior snapshot for the same tab+url so the popup shows the
  // current page state rather than every intermediate scrape.
  const filtered = existing.filter((e) => !(e.tabId === tabId && e.url === pageUrl));
  filtered.push(entry);
  const trimmed = filtered.length > MAX_PAGEDATA ? filtered.slice(-MAX_PAGEDATA) : filtered;
  await chrome.storage.local.set({ [PAGEDATA_KEY]: trimmed });
  return { count: entry.rows.length };
}

chrome.runtime.onInstalled.addListener(async () => {
  const cur = await chrome.storage.local.get([NETWORK_KEY, PAGEDATA_KEY, MESSAGES_KEY]);
  const writes = {};
  if (!cur[NETWORK_KEY]) writes[NETWORK_KEY] = [];
  if (!cur[PAGEDATA_KEY]) writes[PAGEDATA_KEY] = [];
  if (!cur[MESSAGES_KEY]) writes[MESSAGES_KEY] = [];
  if (Object.keys(writes).length) await chrome.storage.local.set(writes);
});

// -------------------------------------------------- hand-index reconcile
//
// A periodic chrome.alarms tick keeps the per-hand sync index in sync
// with the live messages capture, so the per-round badges in History
// stay accurate even when the History tab isn't open. The actual
// network flush is now strictly user-initiated (Flush now button) —
// auto-flush was removed 2026-05-21.
const RECONCILE_ALARM = "casino-reconcile";
chrome.alarms.create(RECONCILE_ALARM, { periodInMinutes: 5, delayInMinutes: 1 });

// -------------------------------------------------- service-worker heartbeat
//
// MV3 service workers are killed by Chrome after ~30s of idle. Webrequest
// + content-script messages wake us on real casino.org activity, but if
// a casino tab is open and quietly sitting on a long-lived WebSocket with
// no per-frame chrome.* round-trips, the SW can still tear down. A
// minimum-period chrome.alarms tick (Chrome enforces 0.5min minimum in
// production builds) is the documented MV3 keep-alive pattern: the
// alarm wake event itself is the keep-alive, the handler does nothing.
//
// This is independent of `captureEnabled` — capture is gated on the
// flag inside the webRequest/onMessage handlers, but we always want
// the worker reachable so toggling capture back on is instant.
const KEEPALIVE_ALARM = "casino-keepalive";
chrome.alarms.create(KEEPALIVE_ALARM, { periodInMinutes: 0.5 });

chrome.alarms.onAlarm.addListener(async (alarm) => {
  if (alarm.name === KEEPALIVE_ALARM) return;  // wake-only, no work
  if (alarm.name !== RECONCILE_ALARM) return;
  if (!Flush) return;
  try {
    await Flush.refreshIndex();
  } catch (e) {
    console.warn("[CASINO] reconcile alarm error:", e);
  }
});
