/**
 * content_script.js  –  Chameleon content script
 *
 * Responsibilities:
 *  1. Accessibility preferences (enlargeInteractive, misclick detection, image AI descriptions)
 *  2. Auto-configure from Hub login page
 *  3. Click / hover tracking → Statsig
 *  4. Head-tracking: focus tracking + INSERT_TEXT dictation + mode HUD
 *
 * NOTE: This is the ONLY content script that handles Head-tracking.
 *       Do NOT also inject content_handsfree.js – that would create duplicate
 *       INSERT_TEXT handlers and duplicate focus events.
 */

console.log("[Chameleon] Content script loaded.");

// ═══════════════════════════════════════════════════════════════════
//  PREFERENCES
// ═══════════════════════════════════════════════════════════════════

let _enlargeInteractive = false;
let _aiAltText = false;

chrome.runtime.sendMessage({ type: "GET_PREFERENCES" }, response => {
  if (response?.preferences) {
    _enlargeInteractive = !!response.preferences.enlargeInteractive;
    _aiAltText = !!response.preferences.aiAltText;
  }
});

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type === "PING_CHAMELEON_CONTENT_SCRIPT") {
    sendResponse({ ok: true });
    return false;
  }

  // Camera permission priming. The background service worker asks the
  // content script (running in the user's active http(s) tab) to inject an
  // invisible iframe whose src is chrome-extension://<id>/requestPermissions.html
  // with allow="camera; microphone". The iframe runs requestPermissions.js
  // which calls navigator.mediaDevices.getUserMedia(...) for media grants.
  // Because the iframe is
  // hosted inside a real tab (with UX surface), Chrome shows the permission
  // prompt as "<extension name> wants to use your camera". After grant, the
  // permission is bound to the chrome-extension:// origin and the offscreen
  // document inherits it. This is the canonical Google sample pattern.
  if (message.type === "CHAMELEON_PRIME_CAMERA") {
    primeCameraPermissionViaIframe()
      .then((res) => sendResponse(res))
      .catch((err) => sendResponse({ status: "denied", error: String(err?.message || err) }));
    return true; // keep the channel open for the async response
  }

  if (message.type === "PREFERENCES_UPDATED" && message.preferences) {
    _enlargeInteractive = !!message.preferences.enlargeInteractive;
    const newAiAltText = !!message.preferences.aiAltText;
    if (_aiAltText && !newAiAltText) {
      // turned off → tear down any pending describe + open tooltip
      try { hideTooltip(); } catch (_) {}
    }
    _aiAltText = newAiAltText;
  }

  if (message.type === "DASHBOARD_PREFS_SYNC") {
    const detail = {
      preferences: message.preferences || null,
      conditions: Array.isArray(message.conditions) ? message.conditions : [],
      revision: Number.isFinite(message.revision) ? message.revision : 0,
      sourceId: typeof message.sourceId === "string" ? message.sourceId : "unknown",
    };
    try {
      window.dispatchEvent(new CustomEvent("chameleon-prefs-sync", { detail }));
    } catch (_) {}
  }

  if (message.type === "CHAMELEON_ENABLED_CHANGED") {
    if (!message.enabled) {
      _enlargeInteractive = false;
      _aiAltText = false;
      try { hideTooltip(); } catch (_) {}
      if (typeof _currentlyEnlarged !== "undefined" && _currentlyEnlarged) {
        restoreElement(_currentlyEnlarged);
      }
    } else {
      chrome.runtime.sendMessage({ type: "GET_PREFERENCES" }, response => {
        if (response?.preferences) {
          _enlargeInteractive = !!response.preferences.enlargeInteractive;
          _aiAltText = !!response.preferences.aiAltText;
        }
      });
    }
    sendResponse({ ok: true });
  }

  // ── Head-tracking messages from background ────────────────────────
  if (message.type === "INSERT_TEXT" && message.text) {
    insertDictatedText(message.text);
  }

  if (message.type === "MODE_CHANGED") {
    showModeHUD(String(message.mode || "").toLowerCase());
  }

  if (message.type === "SCROLL_BY" && typeof message.deltaY === "number") {
    window.scrollBy({ top: message.deltaY, behavior: "instant" });
  }

  if (message.type === "SHOW_CONFIRMATION") {
    showDictationOverlay(message.command);
  }

  if (message.type === "HIDE_CONFIRMATION") {
    removeDictationOverlay();
  }

  return true;
});

// ═══════════════════════════════════════════════════════════════════
//  CAMERA PERMISSION PRIMING (iframe-from-content-script)
// ═══════════════════════════════════════════════════════════════════

const CHAMELEON_PERM_IFRAME_ID = "chameleon-camera-permission-iframe";

function primeCameraPermissionViaIframe() {
  return new Promise((resolve) => {
    // If a previous attempt left an iframe behind, remove it.
    const existing = document.getElementById(CHAMELEON_PERM_IFRAME_ID);
    if (existing) existing.remove();

    const iframe = document.createElement("iframe");
    iframe.id = CHAMELEON_PERM_IFRAME_ID;
    iframe.setAttribute("allow", "camera; microphone");
    // IMPORTANT: Chrome refuses to anchor permission prompts to iframes that
    // are display:none, visibility:hidden, or 0x0. The iframe must be at
    // least 1x1 and visible to the layout engine. We make it tiny and pin it
    // off the bottom-right where the user won't notice.
    Object.assign(iframe.style, {
      position: "fixed",
      bottom: "0",
      right: "0",
      width: "1px",
      height: "1px",
      border: "0",
      opacity: "0.01",
      pointerEvents: "none",
      zIndex: "2147483647",
    });
    iframe.src = chrome.runtime.getURL("requestPermissions.html");

    let settled = false;
    function cleanup(result) {
      if (settled) return;
      settled = true;
      window.removeEventListener("message", onMessage);
      if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
      resolve(result);
    }

    function onMessage(event) {
      // Same-process iframe → event.source is the iframe contentWindow.
      if (event.source !== iframe.contentWindow) return;
      const data = event.data;
      if (!data || data.type !== "CHAMELEON_CAMERA_PERMISSION") return;
      cleanup({ status: data.status, error: data.error });
    }

    window.addEventListener("message", onMessage);
    document.documentElement.appendChild(iframe);

    // Safety timeout — if Chrome silently dismisses (e.g. blocked at the
    // chrome://settings/content/camera level), surface that fact instead of
    // hanging.
    setTimeout(() => cleanup({ status: "denied", error: "timeout" }), 60_000);
  });
}

// ═══════════════════════════════════════════════════════════════════
//  AUTO-CONFIGURE FROM HUB
// ═══════════════════════════════════════════════════════════════════

(function autoConfigureFromHub() {
  window.addEventListener("chameleon-prefs-set", e => {
    const payload = e?.detail || {};
    chrome.runtime.sendMessage({
      type: "SYNC_SET_PREFS",
      preferences: payload.preferences,
      conditions: payload.conditions,
      sourceId: payload.sourceId,
      revision: payload.revision,
    }).catch(err => console.warn("[Chameleon] Pref sync failed:", err));
  });

  window.addEventListener("chameleon-ext-configure", e => {
    const onExpectedHubRoute =
      window.location.pathname.startsWith("/login") ||
      window.location.pathname.startsWith("/dashboard");
    if (!onExpectedHubRoute) return;

    const { hubUrl, userToken, handshakeCode } = e.detail || {};
    if (!hubUrl || (!userToken && !handshakeCode)) return;
    chrome.runtime.sendMessage({ type: "AUTO_CONFIGURE", hubUrl, userToken, handshakeCode })
      .catch(err => console.warn("[Chameleon] Auto-configure failed:", err));
  });

  window.addEventListener("chameleon-ext-refresh", () => {
    if (!window.location.pathname.startsWith("/dashboard")) return;
    chrome.runtime.sendMessage({ type: "REFRESH_PREFERENCES" })
      .catch(err => console.warn("[Chameleon] Refresh failed:", err));
  });

  window.addEventListener("chameleon-ext-signout", () => {
    chrome.runtime.sendMessage({ type: "CLEAR_AUTH" })
      .catch(err => console.warn("[Chameleon] Clear auth failed:", err));
  });
})();

// ═══════════════════════════════════════════════════════════════════
//  SHARED HELPER
// ═══════════════════════════════════════════════════════════════════

function sendEventWithAck(payload) {
  try {
    chrome.runtime.sendMessage(payload, response => {
      if (chrome.runtime.lastError) return;
      // ack logged only in debug builds
    });
  } catch (e) {
    console.warn("[Chameleon] sendMessage threw:", e);
  }
}

// (telemetry "content_script_loaded" event removed — no backend in v2.0)

// ═══════════════════════════════════════════════════════════════════
//  IMAGE HOVER + AI DESCRIPTIONS
// ═══════════════════════════════════════════════════════════════════

const IMG_HOVER_COOLDOWN_MS = 2000;
const HOVER_DESC_DELAY_MS   = 5000;
const _imageHoverLastTs  = new WeakMap();
const _imageHoverTimers  = new WeakMap();
const _imageDescCache    = new WeakMap();
let _hoverTooltip   = null;
let _currentHoverEl = null;
let _lastPointer    = { x: 0, y: 0 };

function ensureTooltip() {
  if (_hoverTooltip && _hoverTooltip.parentNode) return _hoverTooltip;
  const t = document.createElement("div");
  t.setAttribute("role", "tooltip");
  Object.assign(t.style, {
    position: "fixed", left: "0px", top: "0px", maxWidth: "480px",
    background: "rgba(0,0,0,0.85)", color: "#fff", padding: "12px 14px",
    borderRadius: "8px", fontSize: "18px", lineHeight: "1.35",
    fontWeight: "600", zIndex: "2147483647", pointerEvents: "none",
    boxShadow: "0 4px 16px rgba(0,0,0,0.35)",
  });
  document.documentElement.appendChild(t);
  _hoverTooltip = t;
  return t;
}

function showTooltip(text, x, y) {
  const t = ensureTooltip();
  t.textContent = text;
  positionTooltip(x, y);
  t.style.opacity = "1";
}

function positionTooltip(x, y) {
  if (!_hoverTooltip) return;
  const offset = 14;
  let left = x + offset, top = y + offset;
  const vw = window.innerWidth, vh = window.innerHeight;
  const rect = _hoverTooltip.getBoundingClientRect();
  if (left + rect.width  > vw - 8) left = Math.max(8, vw - rect.width  - 8);
  if (top  + rect.height > vh - 8) top  = Math.max(8, vh - rect.height - 8);
  _hoverTooltip.style.left = `${Math.round(left)}px`;
  _hoverTooltip.style.top  = `${Math.round(top)}px`;
}

function hideTooltip() {
  if (_hoverTooltip) { _hoverTooltip.remove(); _hoverTooltip = null; }
}

function extractBackgroundImageUrl(el) {
  try {
    const bg = window.getComputedStyle(el).backgroundImage || "";
    const m  = bg.match(/url\(("|'|)(.*?)\1\)/);
    return m?.[2] || null;
  } catch (_) { return null; }
}

function hasMeaningfulAltText(el) {
  if (!el) return false;
  // <img alt="..."> — non-empty, non-whitespace, not a generic placeholder.
  const alt = (el.getAttribute && el.getAttribute("alt")) || "";
  const aria = (el.getAttribute && (el.getAttribute("aria-label") || "")) || "";
  const labelledBy = (el.getAttribute && el.getAttribute("aria-labelledby")) || "";
  const title = (el.getAttribute && el.getAttribute("title")) || "";
  const candidate = (alt || aria || title).trim();
  if (labelledBy) {
    // If labelledby points at a real element with text, treat as labeled.
    const ids = labelledBy.split(/\s+/).filter(Boolean);
    for (const id of ids) {
      const ref = document.getElementById(id);
      if (ref && ref.textContent && ref.textContent.trim().length > 0) return true;
    }
  }
  if (!candidate) return false;
  // Filter out junk placeholders that signal "no real alt".
  const junk = /^(image|photo|picture|img|graphic|icon|logo|untitled|placeholder|.*\.(jpe?g|png|gif|webp|svg))$/i;
  if (junk.test(candidate)) return false;
  return candidate.length >= 3;
}

function handleImageHover(evt) {
  // Off by default — only run when the user has explicitly enabled
  // "AI alt text" in their dashboard settings.
  if (!_aiAltText) return;
  try {
    if (evt.relatedTarget && evt.target?.contains?.(evt.relatedTarget)) return;
    let imgEl = null, src = null;
    if (evt.target instanceof HTMLImageElement) {
      imgEl = evt.target;
      src   = imgEl.currentSrc || imgEl.src || null;
    } else if (evt.target instanceof Element) {
      const bgUrl = extractBackgroundImageUrl(evt.target);
      if (bgUrl) { imgEl = evt.target; src = bgUrl; }
    }
    if (!imgEl || !src) return;

    // Skip the Gemini call if the image already has meaningful alt text or an
    // accessible name — let the existing label do its job. This saves API
    // calls and respects the page author's intent.
    if (hasMeaningfulAltText(imgEl)) return;

    const last = _imageHoverLastTs.get(imgEl) || 0;
    const now  = Date.now();
    if (now - last < IMG_HOVER_COOLDOWN_MS) return;
    _imageHoverLastTs.set(imgEl, now);

    _currentHoverEl = imgEl;
    _lastPointer    = { x: evt.clientX, y: evt.clientY };

    clearTimeout(_imageHoverTimers.get(imgEl));
    const timerId = setTimeout(async () => {
      try {
        let desc = _imageDescCache.get(imgEl);
        if (!desc) {
          const srcUrl = new URL(src, location.href).toString();
          const blob   = await fetch(srcUrl, { mode: "cors" }).then(r =>
            r.ok ? r.blob() : Promise.reject(r.status));
          const reader = new FileReader();
          const base64 = await new Promise(res => {
            reader.onload = () => res(String(reader.result).split(",")[1] || "");
            reader.readAsDataURL(blob);
          });
          desc = await new Promise(res => {
            chrome.runtime.sendMessage(
              { type: "DESCRIBE_IMAGE", base64, mimeType: blob.type || "image/jpeg" },
              resp => res(resp?.ok ? resp.description || "" : ""),
            );
          });
          if (desc) _imageDescCache.set(imgEl, desc);
        }
        if (desc) showTooltip(desc, _lastPointer.x, _lastPointer.y);
      } catch (_) {}
    }, HOVER_DESC_DELAY_MS);
    _imageHoverTimers.set(imgEl, timerId);

    if (!imgEl.dataset.trackerHoverBound) {
      imgEl.addEventListener("mouseleave", () => {
        clearTimeout(_imageHoverTimers.get(imgEl));
        _imageHoverTimers.delete(imgEl);
        if (_currentHoverEl === imgEl) { _currentHoverEl = null; hideTooltip(); }
      });
      imgEl.dataset.trackerHoverBound = "1";
    }
  } catch (_) {}
}

document.addEventListener("mouseover", handleImageHover, true);
document.addEventListener("mousemove", e => {
  _lastPointer = { x: e.clientX, y: e.clientY };
  if (_hoverTooltip) positionTooltip(e.clientX, e.clientY);
}, true);

// ═══════════════════════════════════════════════════════════════════
//  CLICK TRACKING
// ═══════════════════════════════════════════════════════════════════

const SMALL_BTN_MAX_WIDTH  = 240;
const SMALL_BTN_MAX_HEIGHT = 80;
const SMALL_BTN_MAX_AREA   = 26000;
const SMALL_BTN_MIN_SIZE   = 20;
const SMALL_SNAP_DISTANCE  = 56;
const MISCLICK_THRESHOLD   = 4;

const _misclickCounts  = new WeakMap();
let   _currentlyEnlarged = null;

function getMisclickCount(el)    { return _misclickCounts.get(el) || 0; }
function incrementMisclickCount(el) {
  const n = getMisclickCount(el) + 1;
  _misclickCounts.set(el, n);
  return n;
}
function resetMisclickCount(el) { _misclickCounts.delete(el); }

function enlargeElement(el, scale = 1.18, fontScale = 1.14) {
  if (_currentlyEnlarged && _currentlyEnlarged !== el) restoreElement(_currentlyEnlarged);
  try {
    const cs = window.getComputedStyle(el);
    el.dataset.chameleonPrevTransform  = el.style.transform  || "";
    el.dataset.chameleonPrevFontSize   = el.style.fontSize   || "";
    el.dataset.chameleonPrevTransition = el.style.transition || "";
    const newFontPx = Math.max(10, Math.round(parseFloat(cs.fontSize || "16") * fontScale));
    el.style.transition   = (el.style.transition ? el.style.transition + ", " : "") +
                            "transform 120ms ease, font-size 120ms ease";
    el.style.transformOrigin = "center center";
    el.style.transform  = `scale(${scale})`;
    el.style.fontSize   = `${newFontPx}px`;
    _currentlyEnlarged  = el;
  } catch (e) { console.warn("[Chameleon] enlargeElement:", e); }
}

function restoreElement(el) {
  try {
    if (!el) return;
    if (el.dataset.chameleonPrevTransform  !== undefined) el.style.transform  = el.dataset.chameleonPrevTransform;
    if (el.dataset.chameleonPrevFontSize   !== undefined) el.style.fontSize   = el.dataset.chameleonPrevFontSize;
    if (el.dataset.chameleonPrevTransition !== undefined) el.style.transition = el.dataset.chameleonPrevTransition;
    delete el.dataset.chameleonPrevTransform;
    delete el.dataset.chameleonPrevFontSize;
    delete el.dataset.chameleonPrevTransition;
    if (_currentlyEnlarged === el) _currentlyEnlarged = null;
  } catch (_) {}
}

function isElementInteractable(el) {
  if (!el || !(el instanceof Element)) return false;
  const tag   = el.tagName;
  const role  = (el.getAttribute("role")  || "").toLowerCase();
  const type  = (el.getAttribute("type")  || "").toLowerCase();
  const style = window.getComputedStyle(el);
  if (style.pointerEvents === "none" || style.visibility === "hidden" || style.display === "none") return false;
  if (el.hasAttribute("disabled"))  return false;
  if (el.hasAttribute("onclick"))   return true;
  if (el.hasAttribute("tabindex"))  return true;
  if (el.isContentEditable)         return true;
  if (role === "button" || role === "link") return true;
  if (["BUTTON","A","SELECT","TEXTAREA","LABEL"].includes(tag)) return true;
  if (tag === "INPUT" && ["button","submit","radio","checkbox","file","image"].includes(type)) return true;
  if (style.cursor === "pointer")   return true;
  return false;
}

function getClickableAncestor(start) {
  let el = start instanceof Element ? start : null;
  while (el) { if (isElementInteractable(el)) return el; el = el.parentElement; }
  return null;
}

function distanceToRect(x, y, rect) {
  return Math.hypot(Math.max(rect.left - x, 0, x - rect.right),
                    Math.max(rect.top  - y, 0, y - rect.bottom));
}

function isSmallClickable(el) {
  try {
    if (!(el instanceof Element)) return false;
    const rect = el.getBoundingClientRect();
    const w = Math.round(rect.width), h = Math.round(rect.height);
    const tag = el.tagName.toLowerCase();
    const role = (el.getAttribute("role") || "").toLowerCase();
    const minSide = (tag === "a" || role === "link") ? 12 : SMALL_BTN_MIN_SIZE;
    if (w < minSide || h < minSide) return false;
    return (w <= SMALL_BTN_MAX_WIDTH && h <= SMALL_BTN_MAX_HEIGHT && w * h <= SMALL_BTN_MAX_AREA);
  } catch (_) { return false; }
}

function findSmallClickableCandidate(x, y) {
  try {
    let best = null, bestDist = Infinity;
    const consider = (el, strategy) => {
      if (!(el instanceof Element) || !isElementInteractable(el) || !isSmallClickable(el)) return;
      const d = distanceToRect(x, y, el.getBoundingClientRect());
      if (d < bestDist) { best = { el, strategy, dist: d }; bestDist = d; }
    };
    for (const node of document.elementsFromPoint(x, y)) {
      consider(node, "atPoint");
      let anc = node.parentElement;
      for (let i = 0; i < 4 && anc; i++) { consider(anc, "ancestor"); anc = anc.parentElement; }
    }
    if (!best) {
      document.querySelectorAll("button,a[href],input[type=button],input[type=submit],[role=button],[role=link],[onclick],[tabindex]")
        .forEach(c => consider(c, "nearest"));
    }
    return (best && best.dist <= SMALL_SNAP_DISTANCE) ? { el: best.el, strategy: best.strategy } : null;
  } catch (_) { return null; }
}

function buildSelectorPath(el, maxDepth = 5) {
  try {
    const parts = [];
    let node = el, depth = 0;
    while (node && node.nodeType === 1 && depth < maxDepth) {
      let part = node.tagName.toLowerCase();
      if (node.id) { parts.unshift(part + `#${node.id}`); break; }
      const cls = (node.className || "").toString().trim().split(/\s+/).filter(Boolean).slice(0, 2);
      if (cls.length) part += "." + cls.join(".");
      parts.unshift(part);
      node = node.parentElement;
      depth++;
    }
    return parts.join(" > ");
  } catch (_) { return ""; }
}

function getElementDescriptor(el) {
  return {
    tag:       el.tagName.toLowerCase(),
    role:      el.getAttribute("role")       || "",
    type:      el.getAttribute("type")       || "",
    id:        el.id                          || "",
    classes:   (el.className || "").toString(),
    ariaLabel: el.getAttribute("aria-label") || "",
    title:     el.getAttribute("title")      || "",
    name:      el.getAttribute("name")       || "",
    href:      el instanceof HTMLAnchorElement ? el.getAttribute("href") || "" : "",
    text:      (el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 120),
  };
}

function pointInRect(x, y, rect) {
  return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
}

document.addEventListener("click", evt => {
  try {
    const x = evt.clientX, y = evt.clientY;
    let chosen = null;

    for (const node of (typeof evt.composedPath === "function" ? evt.composedPath() : [])) {
      if (node instanceof Element) {
        const cand = getClickableAncestor(node);
        if (cand) { chosen = { el: cand, strategy: "ancestor" }; break; }
      }
    }
    if (!chosen) {
      for (const node of document.elementsFromPoint(x, y)) {
        const cand = getClickableAncestor(node);
        if (cand) { chosen = { el: cand, strategy: "elementsFromPoint" }; break; }
      }
    }
    if (!chosen) {
      let best = null, bestDist = Infinity;
      document.querySelectorAll("button,a[href],[role=button],[onclick],[tabindex]").forEach(c => {
        if (!isElementInteractable(c)) return;
        const d = distanceToRect(x, y, c.getBoundingClientRect());
        if (d < bestDist) { best = c; bestDist = d; }
      });
      if (best && bestDist <= 80) chosen = { el: best, strategy: "nearestByDistance" };
    }

    const smallPref = findSmallClickableCandidate(x, y);
    if (smallPref) chosen = smallPref;

    if (!chosen) {
      const fb = evt.target instanceof Element ? evt.target : document.body;
      chosen = { el: fb, strategy: "rawTarget" };
    }

    const el   = chosen.el;
    const rect = el.getBoundingClientRect();

    if (_enlargeInteractive) {
      const sp = findSmallClickableCandidate(x, y);
      if (sp?.el instanceof Element) {
        const r2 = sp.el.getBoundingClientRect();
        if (!pointInRect(x, y, r2)) {
          const c = incrementMisclickCount(sp.el);
          if (c >= MISCLICK_THRESHOLD && isSmallClickable(sp.el)) {
            enlargeElement(sp.el);
            resetMisclickCount(sp.el);
          }
        } else {
          resetMisclickCount(sp.el);
        }
      }
    }

    // (telemetry "element_clicked" event removed — no backend in v2.0)
  } catch (e) { console.warn("[Chameleon] Click handler:", e); }
}, true);

// (legacy `trackElement` Statsig template + FOCUS_CHANGED relay removed —
//  the native-host backend they fed has been retired.)

function isTextField(el) {
  if (!el || !(el instanceof Element)) return false;
  const tag  = el.tagName.toLowerCase();
  const type = (el.getAttribute("type") || "").toLowerCase();
  const role = (el.getAttribute("role") || "").toLowerCase();
  if (tag === "input") {
    return ["text","search","email","url","password","number","tel",""].includes(type);
  }
  if (tag === "textarea") return true;
  if (el.isContentEditable || el.getAttribute("contenteditable") === "true" || el.getAttribute("contenteditable") === "") return true;
  if (role === "textbox" || role === "search") return true;
  return false;
}

// Stub kept so any remaining listener registrations don't throw — it's a
// no-op now that focus relay is gone.
document.addEventListener("focusin", e => {
  void isTextField(e.target);
}, true);

document.addEventListener("focusout", e => {
  void isTextField(e.target);
}, true);

// ═══════════════════════════════════════════════════════════════════
//  HEAD-TRACKING: TEXT INSERTION
//
//  FIX: The original code appended an extra " " here on top of
//  Python already sending "text + ' '" → double-space every word.
//  Now we insert msg.text exactly as received (Python owns spacing).
// ═══════════════════════════════════════════════════════════════════

function insertDictatedText(text) {
  const el = document.activeElement;
  if (!el) {
    console.warn("[Chameleon] INSERT_TEXT: no active element");
    return;
  }

  if (el.tagName === "INPUT" || el.tagName === "TEXTAREA") {
    const start  = el.selectionStart ?? el.value.length;
    const end    = el.selectionEnd   ?? el.value.length;
    el.value     = el.value.slice(0, start) + text + el.value.slice(end);
    const newPos = start + text.length;
    el.setSelectionRange(newPos, newPos);
    // Dispatch both events so React/Vue/Angular pick up the change
    el.dispatchEvent(new Event("input",  { bubbles: true }));
    el.dispatchEvent(new Event("change", { bubbles: true }));
    return;
  }

  if (el.isContentEditable) {
    document.execCommand("insertText", false, text);
    return;
  }

  console.warn("[Chameleon] INSERT_TEXT: active element is not a text field", el);
}

// ═══════════════════════════════════════════════════════════════════
//  HEAD-TRACKING: MODE HUD
// ═══════════════════════════════════════════════════════════════════

let _modeHUD = null;
let _modeHUDTimer = null;

function showModeHUD(mode) {
  if (!_modeHUD) {
    _modeHUD = document.createElement("div");
    _modeHUD.id = "chameleon-mode-hud";
    Object.assign(_modeHUD.style, {
      position: "fixed", bottom: "20px", right: "20px",
      zIndex: "2147483647", padding: "7px 16px", borderRadius: "20px",
      fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
      fontSize: "13px", fontWeight: "700", color: "#fff",
      pointerEvents: "none", transition: "opacity 0.25s",
      textTransform: "uppercase", letterSpacing: "0.5px",
    });
    document.body.appendChild(_modeHUD);
  }
  const cfg = {
    cursor: { label: "👁 Cursor",  bg: "#10b981" },
    scroll: { label: "↕ Scroll",  bg: "#3b82f6" },
    drag:   { label: "✋ Drag",    bg: "#f59e0b" },
    typing: { label: "⌨ Typing",  bg: "#8b5cf6" },
  }[mode] || { label: mode, bg: "#64748b" };

  _modeHUD.textContent    = cfg.label;
  _modeHUD.style.background = cfg.bg;
  _modeHUD.style.opacity  = "1";

  clearTimeout(_modeHUDTimer);
  _modeHUDTimer = setTimeout(() => { if (_modeHUD) _modeHUD.style.opacity = "0"; }, 2000);
}

// ═══════════════════════════════════════════════════════════════════
//  HEAD-TRACKING: DICTATION CONFIRMATION OVERLAY
// ═══════════════════════════════════════════════════════════════════

let _dictationOverlay = null;

function showDictationOverlay(commandText) {
  removeDictationOverlay();
  _dictationOverlay = document.createElement("div");
  Object.assign(_dictationOverlay.style, {
    position: "fixed", top: "24px", left: "50%",
    transform: "translateX(-50%)",
    backgroundColor: "#0f172a", color: "#fff",
    padding: "16px 24px", borderRadius: "12px",
    zIndex: "2147483647", fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
    boxShadow: "0 10px 25px rgba(0,0,0,0.5)", border: "2px solid #7c3aed",
    textAlign: "center", minWidth: "320px", pointerEvents: "none",
  });
  const cmdRow = document.createElement("div");
  cmdRow.style.cssText = "font-size:18px;font-weight:700;margin-bottom:8px";
  cmdRow.appendChild(document.createTextNode("Command: "));
  const cmdSpan = document.createElement("span");
  cmdSpan.style.color = "#a78bfa";
  cmdSpan.textContent = `"${commandText}"`;
  cmdRow.appendChild(cmdSpan);

  const hintRow = document.createElement("div");
  hintRow.style.cssText = "font-size:14px;color:#cbd5e1;display:flex;justify-content:space-between;gap:16px";
  hintRow.innerHTML =
    '<span>Say <b style="color:#4ade80;background:rgba(74,222,128,.1);padding:2px 6px;border-radius:4px">YES</b> to execute</span>' +
    '<span>Say <b style="color:#f87171;background:rgba(248,113,113,.1);padding:2px 6px;border-radius:4px">NO</b> to type</span>';

  _dictationOverlay.replaceChildren(cmdRow, hintRow);
  document.body.appendChild(_dictationOverlay);
}

function removeDictationOverlay() {
  if (_dictationOverlay) { _dictationOverlay.remove(); _dictationOverlay = null; }
}

// ═══════════════════════════════════════════════════════════════════
//  CRASH CONTEXT HELPERS
//  Exposed on window so telemetry detectors (injected after this script)
//  can call window.__chameleonBuildCrashContext(element) to get a rich,
//  privacy-scrubbed DOM/interaction snapshot.
// ═══════════════════════════════════════════════════════════════════

const _ccKeyHistory = [];
const _ccKeyHistoryMax = 10;
let _ccPrevFocusDesc = null;
let _ccCurrFocusDesc = null;

const _CC_STYLE_ALLOWLIST = [
  "display", "position", "visibility", "overflow", "z-index", "opacity", "pointer-events",
];
const _CC_ARIA_ALLOWLIST = [
  "aria-label", "aria-labelledby", "aria-describedby", "aria-expanded",
  "aria-hidden", "aria-disabled", "aria-selected", "aria-checked",
];

document.addEventListener("keydown", (e) => {
  // Track key name only; character keys recorded as "[char]" to avoid PII
  const key = e.key;
  const entry = { key: key.length === 1 ? "[char]" : key, ts: Date.now() };
  _ccKeyHistory.push(entry);
  if (_ccKeyHistory.length > _ccKeyHistoryMax) _ccKeyHistory.shift();
}, { capture: true, passive: true });

document.addEventListener("focusin", (e) => {
  _ccPrevFocusDesc = _ccCurrFocusDesc;
  if (e.target instanceof Element) {
    _ccCurrFocusDesc = {
      tag: e.target.tagName?.toLowerCase(),
      role: e.target.getAttribute("role") || undefined,
      id: e.target.id || undefined,
    };
  }
}, { capture: true, passive: true });

function _ccSanitizeHtml(html) {
  let s = html
    .replace(/<script[\s\S]*?<\/script>/gi, "")
    .replace(/<style[\s\S]*?<\/style>/gi, "");
  // Mask value attributes in input/textarea tags
  s = s.replace(
    /(<(?:input|textarea)[^>]*?)\s+value\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]*)/gi,
    '$1 value="[redacted]"',
  );
  return s.slice(0, 2000);
}

function _ccGetSourceRef(el) {
  // 1. Prefer explicit data-cham-src annotation
  let cur = el;
  while (cur instanceof Element) {
    const ref = cur.getAttribute("data-cham-src");
    if (ref) return ref;
    cur = cur.parentElement;
  }
  // 2. Fallback: build a CSS selector path from the element up 4 levels
  const parts = [];
  let node = el;
  for (let i = 0; i < 4 && node instanceof Element; i++) {
    const tag = node.tagName.toLowerCase();
    const id = node.id ? `#${node.id}` : "";
    const cls = node.classList?.length > 0 ? `.${node.classList[0]}` : "";
    parts.unshift(`${tag}${id}${cls}`);
    node = node.parentElement;
  }
  return parts.join(" > ") || undefined;
}

function _ccGetComputedStyleSubset(el) {
  try {
    const cs = window.getComputedStyle(el);
    const subset = {};
    _CC_STYLE_ALLOWLIST.forEach(prop => {
      const val = cs.getPropertyValue(prop);
      if (val) subset[prop] = val;
    });
    return Object.keys(subset).length ? subset : undefined;
  } catch {
    return undefined;
  }
}

function _ccGetAriaSnapshot(el) {
  const snap = {};
  _CC_ARIA_ALLOWLIST.forEach(attr => {
    const val = el.getAttribute(attr);
    if (val !== null) snap[attr] = val;
  });
  const role = el.getAttribute("role");
  if (role) snap.role = role;
  return Object.keys(snap).length ? snap : undefined;
}

function _ccGetParentTree(el, depth = 3) {
  const tree = [];
  let cur = el?.parentElement;
  let d = 0;
  while (cur && cur !== document.body && d < depth) {
    tree.push({
      tag: cur.tagName?.toLowerCase(),
      role: cur.getAttribute("role") || undefined,
      id: cur.id || undefined,
      class: (typeof cur.className === "string" ? cur.className.trim().slice(0, 60) : undefined) || undefined,
    });
    cur = cur.parentElement;
    d++;
  }
  return tree.length ? tree : undefined;
}

function _ccGetSelectorPath(el) {
  if (!(el instanceof Element)) return undefined;
  const parts = [];
  let node = el;
  for (let i = 0; i < 3 && node instanceof Element && node !== document.body; i++) {
    const tag = node.tagName.toLowerCase();
    const id = node.id ? `#${node.id}` : "";
    const cls = node.classList?.length > 0 ? `.${node.classList[0]}` : "";
    parts.unshift(`${tag}${id}${cls}`);
    node = node.parentElement;
  }
  return parts.join(" > ") || undefined;
}

function _ccSerializeFocusDesc(desc) {
  if (!desc) return undefined;
  if (typeof desc === "string") return desc;
  const tag = desc.tag || "unknown";
  const id = desc.id ? `#${desc.id}` : "";
  const role = desc.role ? `[${desc.role}]` : "";
  return `${tag}${id}${role}`;
}

window.__chameleonBuildCrashContext = function (element) {
  if (!(element instanceof Element)) return {};
  const outerHtml = typeof element.outerHTML === "string"
    ? _ccSanitizeHtml(element.outerHTML)
    : undefined;
  return {
    source_ref: _ccGetSourceRef(element),
    selector_path: _ccGetSelectorPath(element),
    dom_context: {
      target_outer_html: outerHtml,
      localized_parent_tree: _ccGetParentTree(element),
      computed_style_subset: _ccGetComputedStyleSubset(element),
      aria_snapshot: _ccGetAriaSnapshot(element),
    },
    interaction_context: {
      recent_keys: _ccKeyHistory.slice(-5).map(k => k.key),
      previous_focus: _ccSerializeFocusDesc(_ccPrevFocusDesc),
      current_focus: _ccSerializeFocusDesc(_ccCurrFocusDesc),
    },
  };
};
