// Side panel script: integrated dashboard shell + separate head-tracking controls

// ── VOICE TAB COMMANDS ────────────────────────────────────────────────────────

const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
let voiceRecognition = null;
let voiceActive = false;

const WORD_NUMBERS = {
  one: 1, two: 2, three: 3, four: 4, five: 5,
  six: 6, seven: 7, eight: 8, nine: 9, ten: 10,
};

function parseTabCommand(transcript) {
  const t = transcript.trim().toLowerCase();
  if (t.includes("next tab"))                               return { action: "next" };
  if (t.includes("previous tab") || t.includes("prev tab")) return { action: "prev" };
  if (t.includes("close tab"))                              return { action: "close" };
  if (t.includes("new tab"))                                return { action: "new" };

  // "go to tab 3" / "tab 3" / "open tab 3"
  const digitMatch = t.match(/tab\s+(\d+)/);
  if (digitMatch) return { action: "goto", index: parseInt(digitMatch[1], 10) };

  // "tab two", "tab five", etc.
  for (const [word, num] of Object.entries(WORD_NUMBERS)) {
    if (t.includes(`tab ${word}`)) return { action: "goto", index: num };
  }

  // Voice Click. Strict regex so natural speech ("I want to click that link")
  // does NOT trigger — only bare "click" or short stand-alone variants.
  // Examples that match: "click", "click here", "click now", "click it",
  // "click on it", "go click", "please click". Strips trailing punctuation.
  const cleaned = t.replace(/[.,!?]+$/g, "").trim();
  if (/^(please |go |now )?click( (it|this|that|here|now|on it|on this|on that))?$/i.test(cleaned)) {
    return { action: "voice_click" };
  }
  return null;
}

function setVoiceStatus(text) {
  const el = document.getElementById("voiceCommandStatus");
  if (!el) return;
  if (text) {
    el.textContent = text;
    el.classList.remove("hidden");
  } else {
    el.classList.add("hidden");
  }
}

const VOICE_CONSENT_KEY = "chameleon_voice_consent"; // "granted" | "denied" | undefined
const VOICE_ENABLED_KEY = "chameleon_voice_enabled"; // boolean

async function getVoiceConsent() {
  const r = await chrome.storage.local.get(VOICE_CONSENT_KEY);
  return r[VOICE_CONSENT_KEY];
}

async function setVoiceConsent(value) {
  await chrome.storage.local.set({ [VOICE_CONSENT_KEY]: value });
}

async function getVoiceEnabled() {
  const r = await chrome.storage.local.get(VOICE_ENABLED_KEY);
  return !!r[VOICE_ENABLED_KEY];
}

async function setVoiceEnabled(value) {
  await chrome.storage.local.set({ [VOICE_ENABLED_KEY]: !!value });
}

// Returns "on-device" if Chrome's standardized on-device speech engine is
// available for English, otherwise "cloud" (audio streams to Google).
async function detectVoiceMode() {
  try {
    if (typeof SpeechRecognition?.available === "function") {
      const status = await SpeechRecognition.available({
        langs: ["en-US"],
        processLocally: true,
      });
      // "available" → ready to run on-device. Anything else (downloadable,
      // downloading, unavailable) → fall back to cloud for now.
      return status === "available" ? "on-device" : "cloud";
    }
  } catch (_) {}
  return "cloud";
}

async function startVoiceCommands() {
  if (!SpeechRecognition) {
    setVoiceStatus("Speech recognition not supported in this browser.");
    console.warn("[Voice] webkitSpeechRecognition unavailable on this build");
    return;
  }
  if (voiceActive) return;
  voiceActive = true;
  const mode = await detectVoiceMode();
  console.log("[Voice] mode:", mode);
  voiceRecognition = new SpeechRecognition();
  if (mode === "on-device") voiceRecognition.processLocally = true;
  voiceRecognition.continuous = true;
  voiceRecognition.interimResults = false;
  voiceRecognition.onresult = (event) => {
    const lastIndex = event.results.length - 1;
    const transcript = event.results[lastIndex][0].transcript;
    console.log("[Voice] heard:", transcript);
    const cmd = parseTabCommand(transcript);
    if (!cmd) return;
    console.log("[Voice] matched command:", cmd);
    if (cmd.action === "voice_click") {
      chrome.runtime.sendMessage({ type: "VOICE_CLICK" }).catch(() => {});
    } else {
      chrome.runtime.sendMessage({ type: "TAB_COMMAND", ...cmd }).catch(() => {});
    }
  };
  voiceRecognition.onstart = () => {
    console.log("[Voice] recognition started");
    setVoiceStatus(mode === "on-device"
      ? "🎤 Voice commands active — processed on your device"
      : "🎤 Voice commands active — audio is streamed to Google");
  };
  voiceRecognition.onend = () => {
    console.log("[Voice] recognition ended");
    if (voiceActive) setTimeout(() => { try { voiceRecognition?.start(); } catch (e) { console.warn("[Voice] restart failed:", e); } }, 300);
  };
  voiceRecognition.onerror = (event) => {
    console.warn("[Voice] error:", event.error, event.message || "");
    // Surface common, actionable errors to the user instead of silent retry-loop.
    if (event.error === "not-allowed" || event.error === "service-not-allowed") {
      setVoiceStatus("⚠️ Microphone permission denied. Click the camera/mic icon in Chrome's address bar to allow.");
      voiceActive = false;
      return;
    }
    if (event.error === "audio-capture") {
      setVoiceStatus("⚠️ No microphone detected.");
      voiceActive = false;
      return;
    }
    if (event.error === "network") {
      setVoiceStatus("⚠️ Speech service unreachable — check your internet connection.");
    }
    if (voiceActive) setTimeout(() => { try { voiceRecognition?.start(); } catch (e) { console.warn("[Voice] restart failed:", e); } }, 600);
  };
  try {
    voiceRecognition.start();
  } catch (e) {
    console.error("[Voice] start() threw:", e);
    setVoiceStatus(`⚠️ Could not start: ${e?.message || e}`);
    voiceActive = false;
  }
}

function stopVoiceCommands() {
  voiceActive = false;
  try { voiceRecognition?.stop(); } catch (_) {}
  voiceRecognition = null;
  setVoiceStatus(null);
}

function showVoiceConsentDialog() {
  return new Promise((resolve) => {
    const overlay = document.getElementById("voiceConsentOverlay");
    const accept = document.getElementById("voiceConsentAccept");
    const cancel = document.getElementById("voiceConsentCancel");
    if (!overlay || !accept || !cancel) { resolve(false); return; }
    overlay.classList.remove("hidden");
    const cleanup = (result) => {
      overlay.classList.add("hidden");
      accept.removeEventListener("click", onAccept);
      cancel.removeEventListener("click", onCancel);
      resolve(result);
    };
    const onAccept = () => cleanup(true);
    const onCancel = () => cleanup(false);
    accept.addEventListener("click", onAccept);
    cancel.addEventListener("click", onCancel);
  });
}

function isHeadTrackingActive() {
  return !!(handsFreeBtn && handsFreeBtn.classList.contains("active-mode"));
}

async function handleVoiceToggleChange(checked) {
  const toggle = document.getElementById("voiceCommandsToggle");
  if (!checked) {
    await setVoiceEnabled(false);
    stopVoiceCommands();
    return;
  }
  let consent = await getVoiceConsent();
  if (consent !== "granted") {
    const accepted = await showVoiceConsentDialog();
    if (!accepted) {
      await setVoiceConsent("denied");
      await setVoiceEnabled(false);
      if (toggle) toggle.checked = false;
      return;
    }
    await setVoiceConsent("granted");
  }
  await setVoiceEnabled(true);
  if (isHeadTrackingActive()) {
    startVoiceCommands();
  } else {
    setVoiceStatus("Voice will activate when head-tracking starts.");
  }
}

async function initVoiceCommandsUI() {
  const toggle = document.getElementById("voiceCommandsToggle");
  if (!toggle) return;
  if (!SpeechRecognition) {
    toggle.disabled = true;
    toggle.title = "Speech recognition isn't available in this browser.";
    return;
  }
  toggle.checked = await getVoiceEnabled();
  toggle.addEventListener("change", (e) => handleVoiceToggleChange(e.target.checked));
}

// ── END VOICE TAB COMMANDS ────────────────────────────────────────────────────

const dashboardFrame = document.getElementById("dashboardFrame");
const dashboardFrameMessage = document.getElementById("dashboardFrameMessage");
const signedOutPlaceholder = document.getElementById("signedOutPlaceholder");
const signedOutLoginLink = document.getElementById("signedOutLoginLink");
const toggleHandsFreePanelBtn = document.getElementById("toggleHandsFreePanel");
const handsFreePanel = document.getElementById("handsFreePanel");
const handsFreeBtn = document.getElementById("handsFreeBtn");
const handsFreeState = document.getElementById("handsFreeState");
const clickMethodSelect = document.getElementById("clickMethod");
const DASHBOARD_THEME_KEY = "chameleon_sidepanel_dashboard_theme";

let dashboardLoadTimeout = null;
let dashboardReloadDebounce = null;
let panelMode = "loading"; // loading | dashboard | signed_out
let lastReloadAt = 0;

// Three click methods ship in v2.0:
//   "dwell"        — hover-to-click only (default)
//   "voice"        — voice "click" command only; dwell disabled
//   "voice_dwell"  — both work
function sanitizeClickMethod(method) {
  if (method === "voice" || method === "voice_dwell") return method;
  return "dwell";
}

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

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 getHubBaseUrl() {
  const result = await chrome.storage.local.get("hubUrl");
  let hubUrl = result.hubUrl;
  if (isStaleLocalhostHubUrl(hubUrl)) {
    // One-shot migration so the side panel doesn't show
    // "localhost refused to connect" after upgrading from a dev install.
    hubUrl = PRODUCTION_HUB_URL;
    chrome.storage.local.set({ hubUrl: PRODUCTION_HUB_URL }).catch(() => {});
  }
  return (hubUrl || PRODUCTION_HUB_URL).replace(/\/$/, "");
}

function showDashboardMessage(text, isError = false) {
  if (!dashboardFrameMessage) return;
  dashboardFrameMessage.textContent = text;
  dashboardFrameMessage.classList.toggle("error", isError);
  dashboardFrameMessage.classList.remove("hidden");
}

function hideDashboardMessage() {
  if (!dashboardFrameMessage) return;
  dashboardFrameMessage.classList.add("hidden");
}

function setSignedOutPlaceholder(visible) {
  if (!signedOutPlaceholder) return;
  signedOutPlaceholder.classList.toggle("show", !!visible);
}

async function loadEmbeddedDashboard(forceReload = false) {
  if (!dashboardFrame) return;

  if (dashboardLoadTimeout) {
    clearTimeout(dashboardLoadTimeout);
    dashboardLoadTimeout = null;
  }

  const hubBaseUrl = await getHubBaseUrl();
  if (signedOutLoginLink) {
    signedOutLoginLink.href = hubBaseUrl;
  }
  const tokenResult = await chrome.storage.local.get("userToken");
  const extensionToken = tokenResult?.userToken || "";
  const storedThemeResult = await chrome.storage.local.get(DASHBOARD_THEME_KEY);
  const storedTheme = storedThemeResult?.[DASHBOARD_THEME_KEY];
  const themeParam =
    storedTheme === "dark" || storedTheme === "light"
      ? `&theme=${encodeURIComponent(storedTheme)}`
      : "";
  const tokenParam = extensionToken ? `&ext_token=${encodeURIComponent(extensionToken)}` : "";

  if (!extensionToken) {
    if (dashboardLoadTimeout) {
      clearTimeout(dashboardLoadTimeout);
      dashboardLoadTimeout = null;
    }
    setSignedOutPlaceholder(true);
    showDashboardMessage("You need to log in on the Chameleon website to use this extension.");
    dashboardFrame.src = "about:blank";
    panelMode = "signed_out";
    return;
  }

  panelMode = "loading";
  setSignedOutPlaceholder(false);
  const baseUrl = `${hubBaseUrl}/dashboard?source=extension-sidepanel&embed=1${themeParam}${tokenParam}`;
  const dashboardUrl = forceReload ? `${baseUrl}&ts=${Date.now()}` : baseUrl;

  showDashboardMessage("Loading dashboard...");
  dashboardFrame.src = dashboardUrl;

  dashboardLoadTimeout = setTimeout(() => {
    showDashboardMessage(
      "Dashboard is taking longer than expected. Make sure the hub is running and configured.",
      true
    );
  }, 8000);
}

function scheduleDashboardReload() {
  const now = Date.now();
  if (now - lastReloadAt < 400) return;
  lastReloadAt = now;
  loadEmbeddedDashboard(true);
}

// When head-tracking starts, voice tab commands are bundled with it.
// We only need explicit user consent if the speech engine will run cloud-side
// (audio leaving the device). On-device speech doesn't trigger any
// third-party transfer, so we just enable it silently.
async function autoEnableVoiceWithHandsFree() {
  if (!SpeechRecognition) return; // platform doesn't support it; nothing to do
  const existingConsent = await getVoiceConsent();
  const mode = await detectVoiceMode();

  if (mode === "on-device") {
    // No third-party transfer → no consent required. Silently enable.
    await setVoiceConsent("granted");
    await setVoiceEnabled(true);
    const toggle = document.getElementById("voiceCommandsToggle");
    if (toggle) toggle.checked = true;
    startVoiceCommands();
    return;
  }

  // Cloud mode: require explicit consent.
  if (existingConsent === "granted") {
    await setVoiceEnabled(true);
    const toggle = document.getElementById("voiceCommandsToggle");
    if (toggle) toggle.checked = true;
    startVoiceCommands();
    return;
  }

  if (existingConsent === "denied") {
    // User previously declined cloud voice. Don't pester them again.
    return;
  }

  // No prior decision and audio would go to Google → ask now.
  const accepted = await showVoiceConsentDialog();
  if (accepted) {
    await setVoiceConsent("granted");
    await setVoiceEnabled(true);
    const toggle = document.getElementById("voiceCommandsToggle");
    if (toggle) toggle.checked = true;
    startVoiceCommands();
  } else {
    await setVoiceConsent("denied");
    await setVoiceEnabled(false);
  }
}

function updateHandsFreeUI(isActive) {
  if (!handsFreeBtn || !handsFreeState) return;

  if (isActive) {
    handsFreeBtn.textContent = "Stop Head-tracking Mode";
    handsFreeBtn.classList.add("active-mode");
    handsFreeState.textContent = "Active";
    autoEnableVoiceWithHandsFree();
  } else {
    handsFreeBtn.textContent = "Start Head-tracking Mode";
    handsFreeBtn.classList.remove("active-mode");
    handsFreeState.textContent = "Inactive";
    stopVoiceCommands();
  }
  // Tracky now runs in an offscreen document (managed by background.js),
  // not in this side panel. The offscreen doc persists when the side panel
  // closes, so head-tracking continues even with the panel closed. The
  // iframe in popup.html is unused; keep it hidden.
  const trackyFrame = document.getElementById("tracky-host-frame");
  if (trackyFrame) {
    trackyFrame.style.display = "none";
    if (trackyFrame.src && trackyFrame.src !== "about:blank") trackyFrame.src = "about:blank";
  }
}

async function loadHandsFreeStatus() {
  if (handsFreeState) handsFreeState.textContent = "Checking status...";
  try {
    const response = await chrome.runtime.sendMessage({ type: "GET_HANDSFREE_STATUS" });
    updateHandsFreeUI(!!response?.isActive);
  } catch (_) {
    if (handsFreeState) handsFreeState.textContent = "Unavailable";
  }
}

async function toggleHandsFree() {
  if (!handsFreeBtn) return;
  const turningOff = handsFreeBtn.classList.contains("active-mode");
  handsFreeBtn.disabled = true;
  handsFreeBtn.textContent = turningOff ? "Stopping..." : "Starting...";

  try {
    const response = await chrome.runtime.sendMessage({ type: "TOGGLE_HANDSFREE" });
    if (response?.error) {
      if (handsFreeState) handsFreeState.textContent = `Error: ${response.error}`;
      await loadHandsFreeStatus();
    } else if (response?.isActive && !turningOff) {
      if (handsFreeState) handsFreeState.textContent = "Warming up camera...";
      // Return keyboard focus to the active tab. Without this, the side
      // panel keeps focus, and dwell-clicks on the page feel unresponsive
      // until the user manually clicks the tab once.
      try {
        const tabs = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
        const tab = tabs && tabs[0];
        if (tab && tab.id !== undefined && tab.windowId !== undefined) {
          await chrome.windows.update(tab.windowId, { focused: true });
          await chrome.tabs.update(tab.id, { active: true });
          // chrome.tabs.update only marks the tab active in its window —
          // it doesn't shift keyboard focus away from the side panel. Inject
          // a focus call into the page so it actually steals focus.
          if (/^https?:/i.test(tab.url || "")) {
            try {
              await chrome.scripting.executeScript({
                target: { tabId: tab.id },
                func: () => {
                  window.focus();
                  // Some pages have a focusable wrapper (body, main); poke
                  // body so the active element resolves to something page-side.
                  try { document.body && document.body.focus({ preventScroll: true }); } catch (_) {}
                },
              });
            } catch (_) {}
          }
          // Blur the side panel so its window-level focus releases.
          try { window.blur?.(); } catch (_) {}
        }
      } catch (_) {}
      setTimeout(() => loadHandsFreeStatus(), 2500);
    } else {
      await loadHandsFreeStatus();
    }
  } catch (error) {
    if (handsFreeState) handsFreeState.textContent = `Error: ${error?.message || "Unknown"}`;
    await loadHandsFreeStatus();
  } finally {
    handsFreeBtn.disabled = false;
  }
}

function wireEvents() {
  if (dashboardFrame) {
    dashboardFrame.addEventListener("load", () => {
      if (dashboardLoadTimeout) {
        clearTimeout(dashboardLoadTimeout);
        dashboardLoadTimeout = null;
      }
      if (panelMode === "loading") {
        panelMode = "dashboard";
        hideDashboardMessage();
      }
    });
  }

  if (toggleHandsFreePanelBtn && handsFreePanel) {
    toggleHandsFreePanelBtn.addEventListener("click", () => {
      handsFreePanel.classList.toggle("hidden");
    });
  }

  if (handsFreeBtn) {
    handsFreeBtn.addEventListener("click", toggleHandsFree);
  }

  if (clickMethodSelect) {
    chrome.storage.local.get("click_method", data => {
      const method = sanitizeClickMethod(data.click_method);
      clickMethodSelect.value = method;
      if (data.click_method !== method) {
        chrome.storage.local.set({ click_method: method });
      }
    });

    clickMethodSelect.addEventListener("change", e => {
      const method = sanitizeClickMethod(e.target.value);
      clickMethodSelect.value = method;
      chrome.storage.local.set({ click_method: method });
      chrome.runtime.sendMessage({ type: "SET_CLICK_METHOD", method });
      // Voice Click and Voice + Dwell both REQUIRE the voice loop. If the
      // user picks one of those modes, auto-enable voice (with consent
      // prompt for cloud transcription); on-device speech enables silently.
      if (clickMethodNeedsVoice(method)) {
        autoEnableVoiceWithHandsFree();
      }
    });
  }


  window.addEventListener("message", event => {
    const data = event?.data;
    if (!data) return;

    if (data.type === "CHAMELEON_DASHBOARD_THEME") {
      if (data.source !== "chameleon-dashboard") return;
      if (data.theme !== "dark" && data.theme !== "light") return;
      chrome.storage.local.set({ [DASHBOARD_THEME_KEY]: data.theme });
      return;
    }

    if (data.type === "CHAMELEON_PREFS_SET") {
      chrome.runtime.sendMessage({
        type: "SYNC_SET_PREFS",
        preferences: data.preferences,
        conditions: data.conditions,
        sourceId: data.sourceId,
        revision: data.revision,
      }).catch(() => {});
    }
  });

  chrome.runtime.onMessage.addListener(message => {
    if (message?.type === "CHAMELEON_AUTH_CHANGED") {
      scheduleDashboardReload();
      return;
    }
    if (message?.type === "PYTHON_STATUS") {
      if (message.status === "stopped") updateHandsFreeUI(false);
      if (message.status === "running") updateHandsFreeUI(true);
      return;
    }
    if (message?.type === "CHAMELEON_PREFS_SYNC") {
      try {
        if (!dashboardFrame?.contentWindow) return;
        dashboardFrame.contentWindow.postMessage(
          {
            type: "CHAMELEON_PREFS_SYNC",
            preferences: message.preferences,
            conditions: message.conditions,
            revision: message.revision,
            sourceId: message.sourceId,
          },
          "*"
        );
      } catch {}
    }
  });

  chrome.storage.onChanged.addListener((changes, areaName) => {
    if (areaName !== "local") return;
    if (!changes.hubUrl && !changes.userToken) return;
    if (dashboardReloadDebounce) clearTimeout(dashboardReloadDebounce);
    dashboardReloadDebounce = setTimeout(() => scheduleDashboardReload(), 150);
  });
}

async function init() {
  wireEvents();
  await initVoiceCommandsUI();
  await Promise.all([loadEmbeddedDashboard(false), loadHandsFreeStatus()]);
}

init();
