// background.js  –  Chameleon service worker

// --- ENABLE SIDE PANEL ON CLICK ---
if (chrome.sidePanel && chrome.sidePanel.setPanelBehavior) {
  chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })
    .catch((error) => console.error("[BG] Error setting side panel behavior:", error));
}

// ── IMPORTS ──────────────────────────────────────────────────────────────────
try { importScripts("config.js"); } catch (_) { console.warn("config.js not found"); }

// ── CONFIGURATION ─────────────────────────────────────────────────────────────
const __cfg = (typeof self !== "undefined" && self.__DDE_CONFIG) || {};
const GEMINI_API_KEY     = __cfg.GEMINI_API_KEY     || "";
const GEMINI_MODEL       = __cfg.GEMINI_MODEL       || "models/gemini-2.0-flash";

let HUB_URL    = __cfg.HUB_URL    || "";
let USER_TOKEN = __cfg.USER_TOKEN || "";

const DEFAULT_PREFERENCES = {
  buttonSize: 48,
  fontSize: 16,
  contrast: "normal",
  spacing: "default",
  reduceMotion: false,
  focusHighlight: false,
  focusRulerColor: "#000000",
  focusRulerHeight: 60,
  focusRulerOpacity: 0.55,
  hideDistractingUI: false,
  dyslexiaFriendly: false,
  letterSpacing: "normal",
  lineHeight: "normal",
  underlineLinks: false,
  highVisibilityFocusRing: false,
  adhdOversizedCursor: false,
  adhdCursorColor: "black",
  oversizedCursor: false,
  cursorColor: "black",
  globalZoom: 1.0,
  aiAltText: false,
  enlargeInteractive: false,
  extraButtonGap: false,
  dwellClickSim: false,
  handsFreeMode: false,
  // --- NEW FEATURES ---
  tintedOverlay: false,
  overlayColor: "peach",
  bionicReading: false
};

// ── STATE ─────────────────────────────────────────────────────────────────────
let userPreferences  = null;
let handsFreeRuntimeState = { handsFreeMode: false, headTrackingEnabled: true, trackingSource: "head" };
let dashboardConditions = [];
let preferencesRevision = 0;
let chameleonEnabled = true;
let preferencesSource = "none"; // none | cache | hub
let lastPreferencesFetchAt = null;
let lastPreferencesFetchOk = null;
let lastPreferencesFetchError = null;
let handsFreeAutoSyncInFlight = false;
// (telemetry queue + flush state removed in v2.0 — see commit history)

function sanitizeClickMethod(method) {
  if (method === "voice" || method === "voice_dwell") return method;
  return "dwell";
}

function clickMethodEnablesDwell(method) {
  return method === "dwell" || method === "voice_dwell";
}

function clickMethodEnablesVoiceClick(method) {
  return method === "voice" || method === "voice_dwell";
}

// Syncs the side-panel click_method setting into userPreferences.dwellClickSim,
// broadcasts PREFERENCES_UPDATED so content scripts enable/disable dwell, and
// fires DASHBOARD_PREFS_SYNC so the dashboard "Enable dwell click" toggle
// reflects the change immediately.
function applyClickMethodToPrefs(method) {
  const dwellOn = clickMethodEnablesDwell(method);
  if (!userPreferences) userPreferences = { ...DEFAULT_PREFERENCES };
  if (userPreferences.dwellClickSim === dwellOn) return; // no change
  userPreferences.dwellClickSim = dwellOn;
  chrome.tabs.query({}, tabs => {
    tabs.forEach(tab => {
      if (tab.id === undefined) return;
      if (chameleonEnabled) {
        chrome.tabs.sendMessage(tab.id, { type: "PREFERENCES_UPDATED", preferences: userPreferences }).catch(() => {});
      }
      // Always tell the dashboard so its "Enable dwell click" toggle stays in sync.
      // revision: 0 bypasses the dashboard's stale-revision guard (which uses
      // Date.now()-based revisions, incompatible with the integer preferencesRevision).
      chrome.tabs.sendMessage(tab.id, {
        type: "DASHBOARD_PREFS_SYNC",
        preferences: userPreferences,
        conditions: dashboardConditions,
        revision: 0,
        sourceId: "click_method",
      }).catch(() => {});
    });
  });
}

function syncRuntimeHandsFreeStateFromPreferences(prefs) {
  const enabledFromPrefs = !!prefs?.handsFreeMode;
  // One-way sync: prefs can ENABLE handsfree but never DISABLE it. The user's
  // explicit Stop click goes through handleHandsFreeToggle which sets the
  // runtime flag to false directly. Without this guard, every dashboard
  // change pushes a fresh prefs payload and the gatekeeper stops the
  // offscreen doc mid-session.
  if (enabledFromPrefs) {
    handsFreeRuntimeState.handsFreeMode = true;
  }
  if (typeof handsFreeRuntimeState.headTrackingEnabled !== "boolean") {
    handsFreeRuntimeState.headTrackingEnabled = true;
  }
  if (!handsFreeRuntimeState.trackingSource) {
    handsFreeRuntimeState.trackingSource = "head";
  }
}

// ── INIT ──────────────────────────────────────────────────────────────────────
(async () => {
  await loadConfig();

  const toggleStored = await chrome.storage.local.get("chameleon_enabled");
  if (toggleStored.chameleon_enabled === false) chameleonEnabled = false;

  const trackingStored = await chrome.storage.local.get("tracking_source");
  if (trackingStored.tracking_source === "head" || trackingStored.tracking_source === "eye") {
    handsFreeRuntimeState.trackingSource = trackingStored.tracking_source;
  }

  const stored = await chrome.storage.local.get("chameleon_user_preferences");
  if (stored.chameleon_user_preferences) {
    userPreferences = normalizeLegacyAutoSeededPrefs(stored.chameleon_user_preferences);
    syncRuntimeHandsFreeStateFromPreferences(userPreferences);
    preferencesSource = "cache";
    await chrome.storage.local.set({ chameleon_user_preferences: userPreferences });
    await syncHandsFreeWithPreferences("cache_bootstrap");
  }

  const storedDashboardState = await chrome.storage.local.get([
    "chameleon_user_conditions",
    "chameleon_prefs_revision",
  ]);
  dashboardConditions = Array.isArray(storedDashboardState.chameleon_user_conditions)
    ? storedDashboardState.chameleon_user_conditions.filter(x => typeof x === "string")
    : [];
  preferencesRevision = Number.isFinite(storedDashboardState.chameleon_prefs_revision)
    ? Number(storedDashboardState.chameleon_prefs_revision)
    : 0;

  // Keep dwell preference in sync with the side-panel click-method selector.
  const clickStored = await chrome.storage.local.get("click_method");
  if (typeof clickStored.click_method === "string") {
    applyClickMethodToPrefs(sanitizeClickMethod(clickStored.click_method));
  }

  fetchUserPreferences();
  ensureContentScriptBridge();
})();

chrome.runtime.onInstalled.addListener(() => {
  ensureContentScriptBridge();
});

chrome.runtime.onStartup.addListener(() => {
  ensureContentScriptBridge();
});

// ── MESSAGE ROUTER ────────────────────────────────────────────────────────────
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  // Intercept the dedicated hardware toggles from the dashboard
  if (message.type === "SET_HANDSFREE_RUNTIME") {
    handsFreeRuntimeState.handsFreeMode = !!message.handsFreeMode;
    handsFreeRuntimeState.headTrackingEnabled = !!message.headTrackingEnabled;
    syncHandsFreeWithPreferences("runtime_toggle");
    sendResponse({ ok: true });
    return true;
  }
  
  // Suppress noisy per-frame messages in the log so diag output stays readable.
  const NOISY = new Set(["FOCUS_CHANGED", "CURSOR_UPDATE", "SCROLL_BY"]);
  if (!NOISY.has(message?.type)) {
    console.log(`[BG] ${message.type}`, message);
  }

  // Offscreen asks which tracker to load on bootstrap.
  if (message.type === "TRACKER_GET_SOURCE") {
    sendResponse({ source: handsFreeRuntimeState.trackingSource || "head" });
    return false;
  }

  // Popup writes the user's tracking-source choice. We persist it, update
  // runtime state, and tell any live offscreen doc to swap trackers in place.
  if (message.type === "SET_TRACKING_SOURCE") {
    const source = message.source === "eye" ? "eye" : "head";
    handsFreeRuntimeState.trackingSource = source;
    chrome.storage.local.set({ tracking_source: source }).catch(() => {});
    chrome.runtime.sendMessage({ type: "TRACKER_SET_SOURCE", source }).catch(() => {});
    sendResponse({ ok: true, source });
    return false;
  }

  // Head-tracking toggle
  if (message.type === "TOGGLE_HANDSFREE") {
    handleHandsFreeToggle().then(sendResponse);
    return true;
  }
  if (message.type === "GET_HANDSFREE_STATUS") {
    isHandsFreeActive()
      .then(active => sendResponse({ isActive: !!active }))
      .catch(() => sendResponse({ isActive: false }));
    return true;
  }

  // Relay normalized cursor coords from offscreen → active tab content script.
  if (message.type === "CURSOR_UPDATE") {
    relayCursorToActiveTab(message.x, message.y);
    return false;
  }

  if (message.type === "MODE_CHANGED") {
    relayModeChangeToActiveTab(message.mode);
    return false;
  }

  if (message.type === "SCROLL_BY") {
    relayScrollToActiveTab(message.deltaY);
    return false;
  }

  if (message.type === "TAB_COMMAND") {
    handleTabCommand(message).catch(() => {});
    return false;
  }

  if (message.type === "VOICE_CLICK") {
    handleVoiceClick().catch(() => {});
    return false;
  }

  if (message.type === "HEADTRACK_CALIBRATED") {
    console.log("[BG] Offscreen reports calibration complete.");
    chrome.runtime.sendMessage({ type: "HEADTRACK_CALIBRATED" }).catch(() => {});
    return false;
  }

  if (message.type === "HEADTRACK_LOG") {
    // Pipe offscreen-side status into the service worker console so devs only
    // need one inspector open.
    const tag = message.level === "error" ? console.error : console.log;
    tag(`[Offscreen→BG] ${message.text}`, message.detail || "");
    return false;
  }

  if (message.type === "HEADTRACK_CAMERA_FAILED") {
    console.error("[BG] Offscreen camera failed:", message.error);
    // Cached grant turned out not to persist. Tear down the offscreen doc
    // and retry once with the permission tab.
    handleOffscreenCameraFailure().catch(err =>
      console.error("[BG] Camera-failure recovery error:", err));
    return false;
  }

  // FOCUS_CHANGED: legacy native-host relay removed. Content script still emits
  // these on focus/blur but there's nothing to forward to; swallow silently.
  if (message.type === "FOCUS_CHANGED") {
    return false;
  }

  // Global on/off
  if (message.type === "CHAMELEON_SET_ENABLED") {
    (async () => {
      chameleonEnabled = !!message.enabled;
      await chrome.storage.local.set({ chameleon_enabled: chameleonEnabled });
      chrome.tabs.query({}, tabs => {
        tabs.forEach(tab => {
          chrome.tabs.sendMessage(tab.id, { type: "CHAMELEON_ENABLED_CHANGED", enabled: chameleonEnabled }).catch(() => {});
        });
      });
      if (chameleonEnabled && userPreferences) {
        chrome.tabs.query({}, tabs => {
          tabs.forEach(tab => {
            chrome.tabs.sendMessage(tab.id, { type: "PREFERENCES_UPDATED", preferences: userPreferences }).catch(() => {});
          });
        });
      }
      await syncHandsFreeWithPreferences("enabled_toggle");
      sendResponse({ ok: true });
    })();
    return true;
  }
  if (message.type === "GET_ENABLED") {
    sendResponse({ enabled: chameleonEnabled });
    return false;
  }

  // Auto-configure from Hub login
  if (message.type === "AUTO_CONFIGURE") {
    (async () => {
      const hubUrl    = (message.hubUrl    || "").replace(/\/$/, "");
      const userToken = message.userToken  || "";
      const handshakeCode = message.handshakeCode || "";
      if (!hubUrl || (!userToken && !handshakeCode)) {
        sendResponse({ ok: false, error: "Missing hubUrl and credentials" });
        return;
      }
      if (!isTrustedAutoConfigureRequest(sender, hubUrl)) {
        sendResponse({ ok: false, error: "Rejected AUTO_CONFIGURE from untrusted sender" });
        return;
      }
      let resolvedToken = userToken;
      if (!resolvedToken && handshakeCode) {
        const redeemed = await redeemHandshakeCode(hubUrl, handshakeCode);
        if (!redeemed?.userToken) {
          sendResponse({ ok: false, error: "Handshake redeem failed" });
          return;
        }
        resolvedToken = redeemed.userToken;
      }
      await chrome.storage.local.set({ hubUrl, userToken: resolvedToken });
      HUB_URL = hubUrl; USER_TOKEN = resolvedToken;
      await fetchUserPreferences();
      chrome.runtime.sendMessage({ type: "CHAMELEON_AUTH_CHANGED" }).catch(() => {});
      sendResponse({ ok: true });
    })();
    return true;
  }

  if (message.type === "CLEAR_AUTH") {
    (async () => {
      HUB_URL = "";
      USER_TOKEN = "";
      userPreferences = null;
      dashboardConditions = [];
      preferencesRevision = 0;
      preferencesSource = "none";
      lastPreferencesFetchAt = new Date().toISOString();
      lastPreferencesFetchOk = null;
      lastPreferencesFetchError = null;

      await chrome.storage.local.remove([
        "hubUrl",
        "userToken",
        "chameleon_user_preferences",
        "chameleon_user_conditions",
        "chameleon_prefs_revision",
      ]);
      await syncHandsFreeWithPreferences("auth_cleared");
      chrome.runtime.sendMessage({ type: "CHAMELEON_AUTH_CHANGED" }).catch(() => {});
      sendResponse({ ok: true });
    })();
    return true;
  }

  if (message.type === "SYNC_SET_PREFS") {
    (async () => {
      const incomingPrefs = message?.preferences;
      if (!incomingPrefs || typeof incomingPrefs !== "object") {
        sendResponse({ ok: false, error: "Invalid preferences payload" });
        return;
      }
      const incomingConditions = Array.isArray(message?.conditions)
        ? message.conditions.filter(x => typeof x === "string")
        : [];
      const sourceId = typeof message?.sourceId === "string" ? message.sourceId : "unknown";
      const incomingRevision = Number.isFinite(message?.revision) ? Number(message.revision) : 0;

      const prevRuntime = handsFreeRuntimeState.handsFreeMode;
      const handsFreeExplicitlyChanged =
        !!incomingPrefs.handsFreeMode !== !!userPreferences?.handsFreeMode;

      userPreferences = normalizeLegacyAutoSeededPrefs(incomingPrefs);
      syncRuntimeHandsFreeStateFromPreferences(userPreferences);

      // If the user changed something other than the head-tracking toggle
      // (e.g. clicked a condition card), don't let the stale saved value
      // kill a live tracking session.
      if (prevRuntime && !handsFreeExplicitlyChanged) {
        handsFreeRuntimeState.handsFreeMode = true;
      }

      dashboardConditions = incomingConditions;
      preferencesRevision = Math.max(preferencesRevision + 1, incomingRevision || 0);
      preferencesSource = "cache";

      await chrome.storage.local.set({
        chameleon_user_preferences: userPreferences,
        chameleon_user_conditions: dashboardConditions,
        chameleon_prefs_revision: preferencesRevision,
      });

      if (chameleonEnabled) {
        chrome.tabs.query({}, tabs => {
          tabs.forEach(tab => {
            chrome.tabs.sendMessage(tab.id, { type: "PREFERENCES_UPDATED", preferences: userPreferences }).catch(() => {});
          });
        });
      }
      broadcastDashboardSync(sourceId);
      await syncHandsFreeWithPreferences("local_sync");
      sendResponse({ ok: true, revision: preferencesRevision });
    })();
    return true;
  }

  // Preferences
  if (message.type === "GET_PREFERENCES") {
    sendResponse({
      preferences: chameleonEnabled ? userPreferences : null,
      conditions: dashboardConditions,
      revision: preferencesRevision,
      meta: {
        source: preferencesSource,
        lastFetchAt: lastPreferencesFetchAt,
        lastFetchOk: lastPreferencesFetchOk,
        lastFetchError: lastPreferencesFetchError,
      },
    });
    return true;
  }
  if (message.type === "GET_DEBUG_STATUS") {
    sendResponse({
      enabled: chameleonEnabled,
      hasHubUrl: !!HUB_URL,
      hasUserToken: !!USER_TOKEN,
      hasPreferences: !!userPreferences,
      source: preferencesSource,
      lastFetchAt: lastPreferencesFetchAt,
      lastFetchOk: lastPreferencesFetchOk,
      lastFetchError: lastPreferencesFetchError,
    });
    return true;
  }
  if (message.type === "REFRESH_PREFERENCES") {
    fetchUserPreferences().then(() => sendResponse({ ok: true }));
    return true;
  }

  // Gemini
  if (message.type === "DESCRIBE_IMAGE") {
    handleGeminiDescription(message).then(sendResponse);
    return true;
  }

  // Click method preference — wire dwell vs. manual into userPreferences so
  // content scripts enable/disable dwell click immediately.
  if (message.type === "SET_CLICK_METHOD") {
    const method = sanitizeClickMethod(message.method);
    chrome.storage.local.set({ click_method: method }).catch(() => {});
    applyClickMethodToPrefs(method);
    return false;
  }

  // Unrecognised message — no async response needed.
  return false;
});

// ── HEAD-TRACKING (WEB OFFSCREEN API) ─────────────────────────────────────────

let creatingOffscreen = false;

// Helper to check if head tracking is running. Tracky lives in an offscreen
// document that persists independent of the side panel — closing the side
// panel does NOT stop tracking.
async function isHandsFreeActive() {
  try { return await chrome.offscreen.hasDocument(); }
  catch (_) { return false; }
}

async function handleHandsFreeToggle() {
  const currentlyActive = await isHandsFreeActive();
  if (currentlyActive) {
    // Manual stop should always win over synced prefs.
    handsFreeRuntimeState.handsFreeMode = false;
    console.log("[BG] Stopping head-tracking (manual toggle)...");
    return stopHandsFreeMode();
  }
  // Manual start should survive stale SYNC_SET_PREFS payloads.
  handsFreeRuntimeState.handsFreeMode = true;
  handsFreeRuntimeState.headTrackingEnabled = true;
  return startHandsFreeMode();
}

// Camera permission must be anchored to a tab with a real UX surface.
// Side panels, offscreen docs, AND top-level chrome-extension:// tabs all
// fail (silently dismissed, or grant doesn't persist to the extension
// origin). The canonical Google sample pattern (chrome-extensions-samples
// PR #1056) is to inject a hidden iframe with src=requestPermissions.html
// and allow="camera; microphone" INTO THE USER'S ACTIVE http(s) TAB via the content
// script. The iframe's origin is chrome-extension://<id>, so the prompt
// reads as extension media access and the grants bind to the extension origin.
// After grant, the offscreen document inherits it.
async function ensureCameraPermissionGranted() {
  // We deliberately do NOT trust a cached "granted" flag. Chrome's permission
  // persistence for chrome-extension:// origins is unreliable across sessions,
  // and a stale cache caused us to skip priming and hit "Permission dismissed"
  // in the offscreen doc with no recovery. Instead, always prime via the
  // iframe: if already granted, requestPermissions.js's getUserMedia resolves
  // silently with no prompt; if not, the prompt appears anchored to the tab.
  // Same code path either way — fast when granted, correct when not.

  const [activeTab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
  const isHttp = (url) => typeof url === "string" && /^https?:\/\//i.test(url);
  let primingTab = activeTab && isHttp(activeTab.url) ? activeTab : null;

  if (!primingTab) {
    // Fallback: any http(s) tab in any window.
    const all = await chrome.tabs.query({ url: ["http://*/*", "https://*/*"] });
    primingTab = all[0] || null;
  }

  if (!primingTab) {
    // No http(s) tab. We can't prime without one. Tell the user clearly.
    return {
      ok: false,
      error: "Open any website (e.g. google.com) in the active tab, then click Start Head-tracking again.",
    };
  }

  console.log("[BG] Priming camera permission via iframe in tab", primingTab.id, primingTab.url);

  // Inject the iframe + listener directly into the page via executeScript's
  // func parameter. This bypasses any reliance on contentScript.js already
  // being attached (which it isn't on tabs that existed before reload).
  let scriptResult;
  try {
    const results = await chrome.scripting.executeScript({
      target: { tabId: primingTab.id, allFrames: false },
      world: "MAIN",
      func: async (iframeUrl) => {
        return await new Promise((resolve) => {
          const existing = document.getElementById("chameleon-camera-permission-iframe");
          if (existing) existing.remove();

          const iframe = document.createElement("iframe");
          iframe.id = "chameleon-camera-permission-iframe";
          iframe.allow = "camera; microphone";
          iframe.style.cssText = "position:fixed;bottom:0;right:0;width:1px;height:1px;opacity:0.01;border:0;z-index:2147483647;pointer-events:none;";
          iframe.src = iframeUrl;

          let settled = false;
          function cleanup(result) {
            if (settled) return;
            settled = true;
            window.removeEventListener("message", onMessage);
            if (iframe.parentNode) iframe.remove();
            resolve(result);
          }
          function onMessage(e) {
            if (e.source !== iframe.contentWindow) return;
            if (!e.data || e.data.type !== "CHAMELEON_CAMERA_PERMISSION") return;
            cleanup({
              status: e.data.status,
              error: e.data.error,
              micGranted: !!e.data.micGranted,
              micError: e.data.micError || null,
            });
          }
          window.addEventListener("message", onMessage);
          document.documentElement.appendChild(iframe);
          setTimeout(() => cleanup({ status: "denied", error: "timeout" }), 60000);
        });
      },
      args: [chrome.runtime.getURL("requestPermissions.html")],
    });
    scriptResult = results && results[0] && results[0].result;
  } catch (err) {
    return { ok: false, error: `inject failed: ${err?.message || err}` };
  }

  console.log("[BG] Priming result:", scriptResult);
  if (scriptResult && scriptResult.status === "granted" && scriptResult.micGranted === false) {
    console.warn("[BG] Microphone permission was not granted during priming:", scriptResult.micError);
  }
  if (scriptResult?.status === "granted") return { ok: true };
  return { ok: false, error: scriptResult?.error || "denied" };
}

// Cache so we only auto-inject once per tab. Cleared whenever a tab navigates
// or the tab id is reused for a new page.
const _injectedTabs = new Set();
chrome.tabs?.onUpdated?.addListener?.((tabId, info) => {
  if (info.status === "loading") _injectedTabs.delete(tabId);
});

// Active-tab cache. Avoids hitting chrome.tabs.query on every cursor frame
// (the offscreen pipeline pushes ~30Hz). Refreshed via tab/window listeners.
let _activeTabId = null;
let _lastCursor = { x: 0.5, y: 0.5 };

async function _refreshActiveTab() {
  try {
    const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
    const tab = tabs && tabs[0];
    if (tab && tab.id !== undefined && /^https?:/i.test(tab.url || "")) {
      _activeTabId = tab.id;
      return tab;
    }
    _activeTabId = null;
    return null;
  } catch (_) {
    _activeTabId = null;
    return null;
  }
}

async function _replayLastCursorToActiveTab() {
  if (!await isHandsFreeActive()) return;
  const tab = await _refreshActiveTab();
  if (!tab) return;
  // Immediately push the last known cursor so the content script can mount
  // its overlay on the very first frame after a tab switch.
  await _sendCursorToTab(tab.id, _lastCursor.x, _lastCursor.y);
}

chrome.tabs?.onActivated?.addListener?.(() => { _replayLastCursorToActiveTab(); });
chrome.windows?.onFocusChanged?.addListener?.((winId) => {
  if (winId !== chrome.windows.WINDOW_ID_NONE) _replayLastCursorToActiveTab();
});

async function _injectContentScriptIntoTab(tabId) {
  try {
    await chrome.scripting.executeScript({
      target: { tabId, allFrames: false },
      files: ["vendor/tracky-mouse/tracky-mouse.js", "accessibilityScript.js"],
    });
    await chrome.scripting.insertCSS({
      target: { tabId, allFrames: false },
      files: ["vendor/tracky-mouse/tracky-mouse.css"],
    });
    return true;
  } catch (e) {
    return false;
  }
}

async function _ensureContentScriptInAllTabs() {
  let tabs;
  try { tabs = await chrome.tabs.query({}); } catch (_) { return; }
  for (const tab of tabs) {
    if (!tab || tab.id === undefined) continue;
    if (!/^https?:/i.test(tab.url || "")) continue;
    // Re-inject regardless of _injectedTabs state — orphaned content scripts
    // from a previous extension session won't have live listeners. The
    // executeScript call is idempotent enough (worst case: the script runs
    // twice on the same page; its top-level state is module-scoped and the
    // second run just registers another onMessage listener — harmless).
    _injectedTabs.add(tab.id);
    _injectContentScriptIntoTab(tab.id);
  }
}

async function _sendCursorToTab(tabId, x, y) {
  try {
    await chrome.tabs.sendMessage(tabId, { type: "HEADTRACK_CURSOR", x, y });
  } catch (err) {
    if (_injectedTabs.has(tabId)) return;
    _injectedTabs.add(tabId);
    const ok = await _injectContentScriptIntoTab(tabId);
    if (ok) {
      console.log("[BG] Auto-injected accessibilityScript + tracky-mouse into tab", tabId);
      chrome.tabs.sendMessage(tabId, { type: "HEADTRACK_CURSOR", x, y }).catch(() => {});
    } else {
      console.warn("[BG] Failed to inject accessibilityScript into tab", tabId);
    }
  }
}

async function relayCursorToActiveTab(x, y) {
  _lastCursor.x = x;
  _lastCursor.y = y;
  let tabId = _activeTabId;
  if (tabId == null) {
    const tab = await _refreshActiveTab();
    tabId = tab?.id ?? null;
  }
  if (tabId == null) return;
  _sendCursorToTab(tabId, x, y);
}

async function relayScrollToActiveTab(deltaY) {
  const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
  const tab = tabs && tabs[0];
  if (!tab || tab.id === undefined) return;
  if (!/^https?:/i.test(tab.url || "")) return;
  chrome.tabs.sendMessage(tab.id, { type: "SCROLL_BY", deltaY }).catch(() => {});
}

// Voice "click" command. Gated on the user actually having Voice Click
// (or Voice + Dwell) selected — otherwise saying "click" should be a no-op.
async function handleVoiceClick() {
  const stored = await chrome.storage.local.get("click_method");
  const method = sanitizeClickMethod(stored.click_method);
  if (!clickMethodEnablesVoiceClick(method)) {
    console.log("[BG] VOICE_CLICK ignored — click_method is", method);
    return;
  }
  const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
  const tab = tabs && tabs[0];
  if (!tab || tab.id === undefined) return;
  if (!/^https?:/i.test(tab.url || "")) return;
  chrome.tabs.sendMessage(tab.id, { type: "VOICE_CLICK" }).catch(() => {});
}

async function handleTabCommand({ action, index }) {
  // Resolve the user's window. `currentWindow: true` can return [] from a
  // service worker fired by a side-panel message, so fall back to
  // lastFocusedWindow → most-recent normal window.
  let tabs = await chrome.tabs.query({ lastFocusedWindow: true, windowType: "normal" });
  if (!tabs || tabs.length === 0) {
    const wins = await chrome.windows.getAll({ windowTypes: ["normal"], populate: true });
    const w = wins.sort((a, b) => (b.focused ? 1 : 0) - (a.focused ? 1 : 0))[0];
    tabs = w?.tabs || [];
  }
  tabs.sort((a, b) => a.index - b.index);
  const activeTab = tabs.find(t => t.active) || tabs[0];
  console.log("[BG] handleTabCommand", { action, index, tabCount: tabs.length, activeIndex: activeTab?.index });
  if (!activeTab) {
    console.warn("[BG] handleTabCommand: no active tab found");
    return;
  }

  if (action === "next") {
    const next = tabs[(activeTab.index + 1) % tabs.length];
    await chrome.tabs.update(next.id, { active: true });
    await chrome.windows.update(next.windowId, { focused: true }).catch(() => {});
  } else if (action === "prev") {
    const prev = tabs[(activeTab.index - 1 + tabs.length) % tabs.length];
    await chrome.tabs.update(prev.id, { active: true });
    await chrome.windows.update(prev.windowId, { focused: true }).catch(() => {});
  } else if (action === "goto" && typeof index === "number") {
    const target = tabs[index - 1];
    if (target) {
      await chrome.tabs.update(target.id, { active: true });
      await chrome.windows.update(target.windowId, { focused: true }).catch(() => {});
    }
  } else if (action === "close") {
    await chrome.tabs.remove(activeTab.id);
  } else if (action === "new") {
    await chrome.tabs.create({ windowId: activeTab.windowId });
  }
}

async function startHandsFreeMode() {
  if (await isHandsFreeActive()) return { isActive: true };
  if (creatingOffscreen) return { isActive: true };

  creatingOffscreen = true;
  console.log("[BG] Starting Web Head-Tracking (offscreen host)…");
  try {
    const perm = await ensureCameraPermissionGranted();
    console.log("[BG] Camera permission result:", perm);
    if (!perm.ok) {
      console.warn("[BG] Camera permission not granted:", perm.error);
      return { isActive: false, error: `Camera: ${perm.error}` };
    }

    // Tracky runs in offscreen.html. The offscreen doc persists independent
    // of the side panel, so closing the panel does NOT stop tracking.
    await chrome.offscreen.createDocument({
      url: "offscreen.html",
      reasons: ["USER_MEDIA"],
      justification: "Head-tracking for accessibility",
    });
    console.log("[BG] Offscreen document created.");

    // Proactively (re)inject the content script into every already-open http
    // tab. Without this, tabs that were open BEFORE this extension session
    // started have no live content-script listener — the manifest-declared
    // content_scripts only run on fresh page loads, not retroactively. So
    // switching to such a tab would show no cursor until you reload it.
    _ensureContentScriptInAllTabs().catch(() => {});

    chrome.runtime.sendMessage({ type: "PYTHON_STATUS", status: "running" }).catch(() => {});
    return { isActive: true };
  } catch (err) {
    console.error("[BG] startHandsFreeMode error:", err);
    return { isActive: false, error: err.message };
  } finally {
    creatingOffscreen = false;
  }
}

// Called when offscreen reports getUserMedia failed (e.g. user revoked camera
// via the omnibox mid-session). Tear down the offscreen doc and retry once —
// the retry re-runs priming via the iframe, which will surface a fresh prompt.
let recoveryInFlight = false;
async function handleOffscreenCameraFailure() {
  if (recoveryInFlight) return;
  recoveryInFlight = true;
  try {
    if (await isHandsFreeActive()) {
      await chrome.offscreen.closeDocument();
      console.log("[BG] Closed offscreen after camera failure.");
    }
    console.log("[BG] Re-priming camera permission…");
    const result = await startHandsFreeMode();
    console.log("[BG] Recovery startHandsFreeMode result:", result);
  } finally {
    recoveryInFlight = false;
  }
}

async function stopHandsFreeMode() {
  try { if (await chrome.offscreen.hasDocument()) await chrome.offscreen.closeDocument(); } catch (_) {}
  chrome.runtime.sendMessage({ type: "PYTHON_STATUS", status: "stopped" }).catch(() => {});
  // Tell every tab's content script to remove the virtual cursor overlay.
  chrome.tabs.query({}, tabs => {
    tabs.forEach(tab => {
      if (tab.id !== undefined) {
        chrome.tabs.sendMessage(tab.id, { type: "HEADTRACK_STOP" }).catch(() => {});
      }
    });
  });
  return { isActive: false };
}

async function persistPreferencesToHub(reason = "unknown") {
  if (!HUB_URL || !USER_TOKEN || !userPreferences) return;
  try {
    const res = await fetch(`${HUB_URL}/api/preferences`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${USER_TOKEN}`,
      },
      body: JSON.stringify({
        preferences: userPreferences,
        conditions: dashboardConditions,
      }),
    });
    if (!res.ok) {
      console.warn(`[BG] Pref push failed (${reason}): HTTP ${res.status}`);
    }
  } catch (e) {
    console.warn(`[BG] Pref push failed (${reason}):`, e);
  }
}

async function disableHandsFreePreference(reason = "unknown") {
  if (!userPreferences || userPreferences.handsFreeMode !== true) return;
  userPreferences = {
    ...userPreferences,
    handsFreeMode: false,
  };
  syncRuntimeHandsFreeStateFromPreferences(userPreferences);
  preferencesRevision += 1;
  preferencesSource = "cache";
  await chrome.storage.local.set({
    chameleon_user_preferences: userPreferences,
    chameleon_user_conditions: dashboardConditions,
    chameleon_prefs_revision: preferencesRevision,
  });
  if (chameleonEnabled) {
    chrome.tabs.query({}, tabs => {
      tabs.forEach(tab => {
        chrome.tabs.sendMessage(tab.id, { type: "PREFERENCES_UPDATED", preferences: userPreferences }).catch(() => {});
      });
    });
  }
  broadcastDashboardSync(`handsfree_${reason}`);
  await persistPreferencesToHub(`handsfree_${reason}`);
}

async function syncHandsFreeWithPreferences(reason = "unknown") {
  if (handsFreeAutoSyncInFlight) return;
  handsFreeAutoSyncInFlight = true;
  try {
    // THE ULTIMATE GATEKEEPER: Check BOTH the runtime state and the saved preferences
    const isHandsFreeOn = handsFreeRuntimeState.handsFreeMode || userPreferences?.handsFreeMode;
    const isHeadTrackingOn = handsFreeRuntimeState.headTrackingEnabled || userPreferences?.headTrackingEnabled;

    const shouldRunHandsFree = !!(chameleonEnabled && isHandsFreeOn && isHeadTrackingOn);
    const isActive = await isHandsFreeActive();

    console.log(`[BG] Hands-Free Gatekeeper (${reason}): \n` + JSON.stringify({
      chameleonGlobalEnabled: chameleonEnabled,
      handsFreeToggle: !!isHandsFreeOn,
      headTrackingToggle: !!isHeadTrackingOn,
      isOffscreenAlreadyRunning: isActive,
      finalDecision: shouldRunHandsFree
    }, null, 2));

    if (shouldRunHandsFree && !isActive) {
      const started = await startHandsFreeMode();
      if (started?.error) {
        console.warn(`[BG] Auto head-tracking start failed (${reason}):`, started.error);
      }
      return;
    }
    if (!shouldRunHandsFree && isActive) {
      await stopHandsFreeMode();
    }
  } catch (err) {
    console.error("[BG] CRASH in syncHandsFreeWithPreferences:", err);
  } finally {
    handsFreeAutoSyncInFlight = false;
  }
}

// ── PREFERENCES ───────────────────────────────────────────────────────────────

// Production hub URL — stale localhost values from earlier dev installs are
// auto-migrated to this so users don't get "localhost refused to connect"
// after upgrading.
const PRODUCTION_HUB_URL = "https://chameleon-orcin.vercel.app";

function isStaleLocalhostHubUrl(value) {
  if (typeof value !== "string" || !value) return false;
  try {
    const host = new URL(value).hostname;
    return host === "localhost" || host === "127.0.0.1" || host === "0.0.0.0";
  } catch (_) {
    return false;
  }
}

async function loadConfig() {
  const result = await chrome.storage.local.get(["hubUrl","userToken"]);
  let storedHubUrl = result.hubUrl;
  if (isStaleLocalhostHubUrl(storedHubUrl)) {
    console.log("[BG] Migrating stale localhost hubUrl to production:", PRODUCTION_HUB_URL);
    storedHubUrl = PRODUCTION_HUB_URL;
    await chrome.storage.local.set({ hubUrl: PRODUCTION_HUB_URL });
  }
  if (storedHubUrl)     HUB_URL    = storedHubUrl;
  if (result.userToken) USER_TOKEN = result.userToken;
}

function ensureContentScriptBridge() {
  if (!chrome.scripting?.executeScript) return;
  chrome.tabs.query({ url: ["http://*/*", "https://*/*"] }, tabs => {
    tabs.forEach(tab => {
      if (!tab.id) return;
      chrome.tabs.sendMessage(tab.id, { type: "PING_CHAMELEON_CONTENT_SCRIPT" }, () => {
        if (!chrome.runtime.lastError) return;
        chrome.scripting
          .executeScript({
            target: { tabId: tab.id, allFrames: true },
            files: ["contentScript.js"],
          })
          .catch(() => {});
      });
    });
  });
}

function isTrustedAutoConfigureRequest(sender, hubUrl) {
  try {
    const senderUrl = sender?.url || sender?.origin || "";
    if (!senderUrl) return false;
    const senderOrigin = new URL(senderUrl).origin;
    const incomingOrigin = new URL(hubUrl).origin;
    if (senderOrigin !== incomingOrigin) return false;

    if (HUB_URL) {
      return new URL(HUB_URL).origin === senderOrigin;
    }

    // If no hub is configured yet, allow only the public hub origin.
    return senderOrigin === "https://chameleon-orcin.vercel.app";
  } catch (_) {
    return false;
  }
}

async function redeemHandshakeCode(hubUrl, code) {
  try {
    const response = await fetch(`${hubUrl}/api/extension-handshake/redeem`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ code }),
    });
    if (!response.ok) return null;
    return await response.json();
  } catch (_) {
    return null;
  }
}

async function fetchUserPreferences() {
  if (!HUB_URL || !USER_TOKEN) return;
  try {
    const res = await fetch(`${HUB_URL}/api/preferences`, {
      headers: {
        Authorization: `Bearer ${USER_TOKEN}`,
      },
    });
    lastPreferencesFetchAt = new Date().toISOString();
    if (res.ok) {
      const data = await res.json();
      userPreferences = normalizeLegacyAutoSeededPrefs(data.preferences);
      syncRuntimeHandsFreeStateFromPreferences(userPreferences);
      dashboardConditions = Array.isArray(data.conditions)
        ? data.conditions.filter(x => typeof x === "string")
        : [];
      preferencesRevision += 1;
      preferencesSource = "hub";
      lastPreferencesFetchOk = true;
      lastPreferencesFetchError = null;
      await chrome.storage.local.set({
        chameleon_user_preferences: userPreferences,
        chameleon_user_conditions: dashboardConditions,
        chameleon_prefs_revision: preferencesRevision,
      });
      if (chameleonEnabled) {
        chrome.tabs.query({}, tabs => {
          tabs.forEach(tab => {
            chrome.tabs.sendMessage(tab.id, { type: "PREFERENCES_UPDATED", preferences: userPreferences }).catch(() => {});
          });
        });
      }
      broadcastDashboardSync("hub_fetch");
      await syncHandsFreeWithPreferences("hub_fetch");
    } else {
      lastPreferencesFetchOk = false;
      lastPreferencesFetchError = `HTTP ${res.status}`;
      console.warn(`[BG] Pref fetch failed: HTTP ${res.status}`);
    }
  } catch (e) {
    lastPreferencesFetchAt = new Date().toISOString();
    lastPreferencesFetchOk = false;
    lastPreferencesFetchError = e?.message || String(e);
    console.warn("[BG] Pref fetch failed:", e);
  }
}

function broadcastDashboardSync(sourceId = "unknown") {
  if (!userPreferences) return;
  chrome.runtime.sendMessage({
    type: "CHAMELEON_PREFS_SYNC",
    preferences: userPreferences,
    conditions: dashboardConditions,
    revision: preferencesRevision,
    sourceId,
  }).catch(() => {});

  chrome.tabs.query({}, tabs => {
    tabs.forEach(tab => {
      chrome.tabs.sendMessage(tab.id, {
        type: "DASHBOARD_PREFS_SYNC",
        preferences: userPreferences,
        conditions: dashboardConditions,
        revision: preferencesRevision,
        sourceId,
      }).catch(() => {});
    });
  });
}

function normalizeLegacyAutoSeededPrefs(prefs) {
  if (!prefs || typeof prefs !== "object") return { ...DEFAULT_PREFERENCES };
  if (isLegacyAutoSeededDefaults(prefs)) {
    console.log("[BG] Normalized legacy auto-seeded defaults to neutral.");
    return { ...DEFAULT_PREFERENCES };
  }

  const normalized = { ...DEFAULT_PREFERENCES, ...prefs };
  normalized.tintedOverlay = coerceBoolean(prefs.tintedOverlay, false);
  normalized.overlayColor = ["peach", "blue", "mint", "yellow"].includes(prefs.overlayColor)
    ? prefs.overlayColor
    : DEFAULT_PREFERENCES.overlayColor;
  return normalized;
}

function coerceBoolean(value, fallback = false) {
  if (typeof value === "boolean") return value;
  if (typeof value === "string") {
    const normalized = value.trim().toLowerCase();
    if (normalized === "true") return true;
    if (normalized === "false") return false;
  }
  if (typeof value === "number") {
    if (value === 1) return true;
    if (value === 0) return false;
  }
  return fallback;
}

function isLegacyAutoSeededDefaults(prefs) {
  return (
    prefs.buttonSize === 55 &&
    prefs.fontSize === 16 &&
    prefs.contrast === "high" &&
    prefs.spacing === "default" &&
    prefs.reduceMotion === false &&
    prefs.focusHighlight === false &&
    (prefs.focusRulerColor === undefined || prefs.focusRulerColor === "amber" || prefs.focusRulerColor === "#000000") &&
    (prefs.focusRulerHeight === undefined || prefs.focusRulerHeight === 28 || prefs.focusRulerHeight === 60) &&
    (prefs.focusRulerOpacity === undefined || prefs.focusRulerOpacity === 0.55) &&
    prefs.hideDistractingUI === false &&
    prefs.dyslexiaFriendly === false &&
    prefs.letterSpacing === "normal" &&
    prefs.lineHeight === "normal" &&
    prefs.underlineLinks === false &&
    prefs.highVisibilityFocusRing === false &&
    (prefs.adhdOversizedCursor === undefined || prefs.adhdOversizedCursor === false) &&
    (prefs.adhdCursorColor === undefined || prefs.adhdCursorColor === "black") &&
    prefs.oversizedCursor === false &&
    (prefs.cursorColor === undefined || prefs.cursorColor === "black") &&
    prefs.globalZoom === 1.0 &&
    prefs.enlargeInteractive === false &&
    prefs.extraButtonGap === false &&
    prefs.dwellClickSim === false &&
    prefs.handsFreeMode === false
  );
}

// ── GEMINI ────────────────────────────────────────────────────────────────────
async function handleGeminiDescription(message) {
  if (!GEMINI_API_KEY) return { ok: false, error: "no_key" };
  try {
    const body = {
      contents: [{
        role: "user",
        parts: [
          { text: "Describe this image concisely (1–3 sentences) for a screen reader. Include main subject, key details, colours/textures, and visible text. Avoid speculation." },
          { inline_data: { mime_type: message.mimeType || "image/jpeg", data: message.base64 } },
        ],
      }],
    };
    const res = await fetch(
      `https://generativelanguage.googleapis.com/v1beta/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`,
      { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) },
    );
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    const json = await res.json();
    const text = json.candidates?.[0]?.content?.parts?.[0]?.text || "No description available.";
    return { ok: true, description: text };
  } catch (e) {
    console.error("[BG] Gemini error:", e);
    return { ok: false, error: e.message };
  }
}
