chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
  if (message.type === "WAOP_COMMAND") {
    await runWAOP(message.payload);
  }
});

async function runWAOP(protocol) {
  if (!protocol || protocol.protocol !== "WAOP") {
    throw new Error("Invalid WAOP protocol");
  }
  for (const step of protocol.steps) {
    try {
      await executeStep(step);
    } catch (err) {
      console.error("[WAOP ERROR]", step.id, err);
      // if (!step.optional) {
      //   break;
      // }
    }
  }
}

async function executeStep(step) {
  const timeout = step.timeout ?? 3000;

  switch (step.type) {
    case "input": {
      const el = await waitForElement(step.selector, timeout, step.text);
      if (!el) throw "Element not found";

      const target = await resolveEditableTarget(el, timeout);
      if (!target) throw "Editable element not found";

      applyInputValue(target, step.value ?? "", el, step.mode);
      break;
    }

    case "click": {
      const el = await waitForElement(step.selector, timeout, step.text);
      if (!el) throw "Element not found";
      el.click();
      break;
    }

    case "select": {
      const el = await waitForElement(step.selector, timeout, step.text);
      if (!el) throw "Element not found";
      el.value = step.value;
      el.dispatchEvent(new Event("change", { bubbles: true }));
      break;
    }

    case "wait": {
      await sleep(step.duration ?? 0);
      break;
    }

    case "waitForElement": {
      const el = await waitForElement(step.selector, timeout, step.text);
      if (!el) throw "Element not found";
      break;
    }

    case "assert": {
      const exists = step.exists ?? true;
      const el = await waitForElement(step.selector, 0, step.text);
      if (exists && !el) throw "Assert failed";
      if (!exists && el) throw "Assert failed";
      break;
    }

    case "highlightText": {
      const targetText = (step.value ?? "").trim();
      if (!targetText) throw "Missing highlight text";

      const searchRoot = step.selector ? await waitForElement(step.selector, timeout, step.text) : document.body;

      if (!searchRoot) throw "Search root not found";

      const range = findTextRange(searchRoot, targetText);
      if (!range) throw "Text not found";

      scrollRangeIntoView(range);
      highlightRange(range);
      break;
    }

    case "hover": {
      const el = await waitForElement(step.selector, timeout, step.text);
      if (!el) throw "Element not found";
      hoverElement(el);
      break;
    }

    case "press": {
      const target = step.selector ? await waitForElement(step.selector, timeout, step.text) : document.activeElement;
      if (!target) throw "Element not found";
      if (typeof target.focus === "function") target.focus();
      const keys =
        Array.isArray(step.keys) && step.keys.length ? step.keys : typeof step.key === "string" && step.key ? [step.key] : typeof step.value === "string" && step.value ? [step.value] : ["Enter"];
      pressKeys(target, keys);
      break;
    }

    case "scroll": {
      await performScroll(step, timeout);
      break;
    }

    case "waitForText": {
      const exists = step.exists ?? true;
      const found = await waitForText(step.value, step.selector, timeout);
      if (exists && !found) throw "Text not found";
      if (!exists && found) throw "Text still present";
      break;
    }

    default:
      throw "Unknown step type";
  }
}

function sleep(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

// 改进 waitForElement 支持文本匹配
async function waitForElement(selector, timeout = 3000, text) {
  const start = Date.now();

  function queryByText(tag, text) {
    const els = document.querySelectorAll(tag);
    return Array.from(els).find((el) => el.textContent?.trim().includes(text)) || null;
  }

  while (Date.now() - start < timeout) {
    // 2️⃣ 如果 text 提供了，按文本匹配 a/button
    if (text) {
      const el = queryByText("a", text) || queryByText("button", text);
      if (el) return el;
    }

    // 1️⃣ 先尝试 selector
    if (selector) {
      const el = document.querySelector(selector);
      if (el) return el;
    }

    await sleep(100);
  }

  return null;
}

function findTextRange(root, text) {
  const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
  const needle = text.toLowerCase();

  while (walker.nextNode()) {
    const node = walker.currentNode;
    const content = node.textContent;
    if (!content) continue;

    const haystack = content.toLowerCase();
    const index = haystack.indexOf(needle);
    if (index !== -1) {
      const range = document.createRange();
      range.setStart(node, index);
      range.setEnd(node, index + text.length);
      return range;
    }
  }

  return null;
}

function scrollRangeIntoView(range) {
  const rect = range.getBoundingClientRect();
  const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
  const targetTop = rect.top + window.scrollY - viewportHeight * 0.3;

  window.scrollTo({
    top: Math.max(targetTop, 0),
    behavior: "smooth",
  });
}

function highlightRange(range) {
  const highlight = document.createElement("span");
  highlight.className = "waop-highlight";
  highlight.style.backgroundColor = "rgba(255, 230, 94, 0.9)";
  highlight.style.borderRadius = "2px";
  highlight.style.padding = "2px 0";
  highlight.style.transition = "background-color 1.2s ease";

  const fragment = range.extractContents();
  highlight.appendChild(fragment);
  range.insertNode(highlight);

  const selectionRange = document.createRange();
  selectionRange.selectNodeContents(highlight);
  const selection = window.getSelection();
  if (selection) {
    selection.removeAllRanges();
    selection.addRange(selectionRange);
  }

  requestAnimationFrame(() => {
    highlight.style.backgroundColor = "rgba(255, 230, 94, 0.25)";
  });

  setTimeout(() => {
    const parent = highlight.parentNode;
    if (!parent) return;

    while (highlight.firstChild) {
      parent.insertBefore(highlight.firstChild, highlight);
    }

    parent.removeChild(highlight);
  }, 2000);
}

function hoverElement(el) {
  const doc = el.ownerDocument || document;
  const view = doc.defaultView || window;
  const init = { bubbles: true, cancelable: true, view };

  el.dispatchEvent(new MouseEvent("mouseover", init));
  el.dispatchEvent(new MouseEvent("mouseenter", init));
  el.dispatchEvent(new MouseEvent("mousemove", init));
}

async function performScroll(step, timeout = 3000) {
  const behavior = step.behavior === "auto" ? "auto" : "smooth";
  const offset = typeof step.offset === "number" ? step.offset : 0;
  const block = step.block || "center";

  if (step.selector) {
    const el = await waitForElement(step.selector, timeout, step.text);
    if (!el) throw "Element not found";
    el.scrollIntoView({ behavior, block, inline: "nearest" });
    if (offset) {
      window.scrollBy({ top: offset, behavior });
    }
    return;
  }

  if (step.to === "top") {
    window.scrollTo({ top: 0, behavior });
    return;
  }

  if (step.to === "bottom") {
    const total = Math.max(document.documentElement?.scrollHeight || 0, document.body?.scrollHeight || 0);
    window.scrollTo({ top: total, behavior });
    return;
  }

  const hasByY = typeof step.byY === "number";
  const hasByX = typeof step.byX === "number";
  const hasBy = typeof step.by === "number";

  if (hasBy || hasByX || hasByY) {
    const deltaY = hasByY ? step.byY : hasBy ? step.by : 0;
    const deltaX = hasByX ? step.byX : 0;
    window.scrollBy({ top: deltaY, left: deltaX, behavior });
    return;
  }

  throw "Missing scroll target";
}

function pressKeys(target, keys) {
  const doc = target.ownerDocument || document;
  const view = doc.defaultView || window;

  keys.forEach((key) => {
    const keyCode = keyToKeyCode(key);
    const code = keyToCode(key);
    const init = {
      key,
      code,
      keyCode,
      which: keyCode,
      bubbles: true,
      cancelable: true,
      composed: true,
      view,
    };

    target.dispatchEvent(new KeyboardEvent("keydown", init));
    target.dispatchEvent(new KeyboardEvent("keypress", init));
    target.dispatchEvent(new KeyboardEvent("keyup", init));
  });
}

function keyToKeyCode(key) {
  const map = {
    Enter: 13,
    Escape: 27,
    Backspace: 8,
    Tab: 9,
    ArrowUp: 38,
    ArrowDown: 40,
    ArrowLeft: 37,
    ArrowRight: 39,
    Delete: 46,
    Space: 32,
  };

  if (map[key]) return map[key];
  if (key && key.length === 1) return key.toUpperCase().charCodeAt(0);
  return 0;
}

function keyToCode(key) {
  const map = {
    Enter: "Enter",
    Escape: "Escape",
    Backspace: "Backspace",
    Tab: "Tab",
    ArrowUp: "ArrowUp",
    ArrowDown: "ArrowDown",
    ArrowLeft: "ArrowLeft",
    ArrowRight: "ArrowRight",
    Delete: "Delete",
    Space: "Space",
  };

  if (map[key]) return map[key];
  if (key && key.length === 1) return `Key${key.toUpperCase()}`;
  return key || "";
}

async function waitForText(text, selector, timeout = 3000) {
  const targetText = (text ?? "").trim();
  if (!targetText) return false;

  const start = Date.now();
  while (Date.now() - start < timeout) {
    const root = selector ? document.querySelector(selector) : document.body;
    if (root && findTextRange(root, targetText)) {
      return true;
    }
    await sleep(100);
  }

  return false;
}

// Resolve legacy iframe/contentEditable editors to a real editable surface.
async function resolveEditableTarget(el, timeout = 3000) {
  if (!el) return null;

  if (el.tagName?.toLowerCase() === "iframe") {
    const doc = await waitForIframeDocument(el, timeout);
    if (!doc) return null;

    if (isEditableElement(doc.body)) return doc.body;

    const iframeEditable = doc.querySelector('[contenteditable="true"], [contenteditable=""], [contenteditable]');
    if (iframeEditable) return iframeEditable;

    return null;
  }

  if (isEditableElement(el) && !isHiddenTextarea(el)) return el;

  if (typeof el.querySelector === "function") {
    const nestedEditable = el.querySelector('[contenteditable="true"], [contenteditable=""], [contenteditable]');
    if (nestedEditable) return nestedEditable;

    const nestedIframe = el.querySelector("iframe");
    if (nestedIframe) return resolveEditableTarget(nestedIframe, timeout);
  }

  const richTextSurface = await findAlternateEditableSurface(el, timeout);
  if (richTextSurface) return richTextSurface;

  return isEditableElement(el) ? el : null;
}

async function waitForIframeDocument(iframe, timeout = 3000) {
  const start = Date.now();

  while (Date.now() - start < timeout) {
    try {
      const doc = iframe.contentDocument || iframe.contentWindow?.document;
      if (doc?.body) return doc;
    } catch (err) {
      console.warn("[WAOP] iframe access blocked", err);
      return null;
    }

    await sleep(100);
  }

  try {
    const fallbackDoc = iframe.contentDocument || iframe.contentWindow?.document;
    return fallbackDoc?.body ? fallbackDoc : null;
  } catch (err) {
    console.warn("[WAOP] iframe access blocked", err);
    return null;
  }
}

function isEditableElement(el) {
  if (!el || !el.tagName) return false;

  if (isContentEditableElement(el)) return true;

  const tag = el.tagName.toLowerCase();
  if (tag === "textarea") return true;
  if (tag === "input") {
    const type = (el.type || "text").toLowerCase();
    return !["button", "submit", "checkbox", "radio", "hidden", "reset", "file"].includes(type);
  }

  return false;
}

function isContentEditableElement(el) {
  if (!el) return false;
  if (el.isContentEditable) return true;

  const tag = el.tagName?.toLowerCase();
  return tag === "body" && el.ownerDocument?.designMode === "on";
}

function applyInputValue(target, value, originEl, mode) {
  const doc = target.ownerDocument || document;
  doc.defaultView?.focus?.();
  target.focus();

  const modeNormalized = mode === "append" ? "append" : "replace";

  if (isContentEditableElement(target)) {
    const current = modeNormalized === "append" ? getContentEditableText(target) : "";
    replaceContentEditableValue(target, `${current}${value}`);
  } else {
    const nextValue = modeNormalized === "append" ? `${target.value ?? ""}${value}` : value;
    setNativeValue(target, nextValue);
  }

  dispatchScopedEvent(target, "input", { data: value, inputType: "insertText" });
  dispatchScopedEvent(target, "change");

  if (originEl && originEl !== target && originEl.tagName?.toLowerCase() === "textarea") {
    const nextOriginValue = modeNormalized === "append" ? `${originEl.value ?? ""}${value}` : value;
    setNativeValue(originEl, nextOriginValue);
    dispatchScopedEvent(originEl, "input", { data: value, inputType: "insertText" });
    dispatchScopedEvent(originEl, "change");
  }
}

function setNativeValue(el, value) {
  try {
    const proto = Object.getPrototypeOf(el);
    const desc = Object.getOwnPropertyDescriptor(proto, "value");
    if (desc?.set) {
      desc.set.call(el, value);
      return;
    }
  } catch (err) {
    // fallback below
  }

  try {
    el.value = value;
  } catch (err) {
    // noop fallback
  }
}

function getContentEditableText(target) {
  return target.textContent ?? "";
}

function dispatchScopedEvent(target, type, extraInit) {
  const doc = target.ownerDocument || document;
  const view = doc.defaultView;

  const baseInit = { bubbles: true, composed: true, cancelable: true, view };
  const init = extraInit ? { ...baseInit, ...extraInit } : baseInit;

  const EventCtor = type === "input" && view?.InputEvent ? view.InputEvent : view?.Event || Event;
  const event = new EventCtor(type, init);
  target.dispatchEvent(event);
}

async function findAlternateEditableSurface(el, timeout) {
  if (!el) return null;

  if (isHiddenTextarea(el)) {
    const iframe = findCompanionIframe(el);
    if (iframe) return resolveEditableTarget(iframe, timeout);
  }

  const siblingEditable = findSiblingEditable(el);
  if (siblingEditable) {
    if (siblingEditable.tagName?.toLowerCase() === "iframe") {
      return resolveEditableTarget(siblingEditable, timeout);
    }

    if (isEditableElement(siblingEditable)) return siblingEditable;
  }

  return null;
}

function isHiddenTextarea(el) {
  if (!el || el.tagName?.toLowerCase() !== "textarea") return false;
  const style = el.ownerDocument?.defaultView?.getComputedStyle?.(el);
  if (!style) return false;
  return style.display === "none" || style.visibility === "hidden" || style.opacity === "0";
}

function findCompanionIframe(el) {
  if (!el.ownerDocument) return null;
  const doc = el.ownerDocument;
  const candidates = [];

  if (el.id) {
    const escapedId = escapeCss(el.id);
    candidates.push(`#${escapedId.replace(/textarea/gi, "iframe")}`);
    candidates.push(`#${escapedId}_iframe`);
    candidates.push(`#${escapedId.replace(/_text$/i, "_iframe")}`);
  }

  if (el.name) {
    const escapedName = escapeCss(el.name);
    candidates.push(`iframe[name="${escapedName}"]`);
  }

  if (el.dataset?.target) {
    const escapedTarget = escapeCss(el.dataset.target);
    candidates.push(`#${escapedTarget}`);
  }

  for (const selector of candidates) {
    const match = doc.querySelector(selector);
    if (match) return match;
  }

  return el.parentElement?.querySelector?.("iframe") || null;
}

function findSiblingEditable(el) {
  let current = el.parentElement;
  let depth = 0;
  while (current && depth < 4) {
    const editable = current.querySelector?.('[contenteditable="true"], [contenteditable=""], [contenteditable], iframe');
    if (editable && editable !== el) return editable;
    current = current.parentElement;
    depth += 1;
  }
  return null;
}

function escapeCss(value) {
  if (typeof CSS !== "undefined" && CSS.escape) {
    return CSS.escape(value);
  }
  return value.replace(/([^a-zA-Z0-9_-])/g, "\\$1");
}

function replaceContentEditableValue(target, value) {
  const doc = target.ownerDocument || document;
  const selection = doc.getSelection?.();
  if (selection) {
    selection.removeAllRanges();
    const range = doc.createRange();
    range.selectNodeContents(target);
    selection.addRange(range);
  }

  let inserted = false;
  try {
    doc.execCommand?.("selectAll", false, null);
    inserted = doc.execCommand?.("insertText", false, value) ?? false;
    if (!inserted) {
      const safeHtml = value.replace(/[&<>\n]/g, (ch) => {
        if (ch === "&") return "&amp;";
        if (ch === "<") return "&lt;";
        if (ch === ">") return "&gt;";
        if (ch === "\n") return "<br>";
        return ch;
      });
      inserted = doc.execCommand?.("insertHTML", false, safeHtml) ?? false;
    }
  } catch (err) {
    inserted = false;
  }

  if (!inserted) {
    target.innerHTML = "";
    const textNodes = value.split(/\n/g);
    textNodes.forEach((line, index) => {
      if (index > 0) target.appendChild(doc.createElement("br"));
      target.appendChild(doc.createTextNode(line));
    });
  }
}
