// Prism Translate — content script: inline bilingual translation + selection translate.
// Self-contained (no ES imports); all provider logic lives in the background worker.

(() => {
  if (window.__prismInjected) return;
  window.__prismInjected = true;

  const SKIP_TAGS = new Set([
    "SCRIPT", "STYLE", "NOSCRIPT", "CODE", "PRE", "KBD", "SAMP", "TEXTAREA",
    "INPUT", "SELECT", "OPTION", "SVG", "CANVAS", "VIDEO", "AUDIO", "IFRAME", "MAP",
  ]);
  const BLOCK_TAGS = [
    "P", "LI", "H1", "H2", "H3", "H4", "H5", "H6", "BLOCKQUOTE", "DD", "DT",
    "FIGCAPTION", "CAPTION", "TH", "TD", "SUMMARY",
  ];
  const BLOCK_SELECTOR = BLOCK_TAGS.join(",");
  const MAX_TARGETS = 1200;

  let cfg = null;
  let pageActive = false;

  // ---------- messaging ----------
  function sendBg(msg) {
    return new Promise((resolve) => {
      chrome.runtime.sendMessage(msg, (r) => {
        if (chrome.runtime.lastError) resolve({ ok: false, error: chrome.runtime.lastError.message });
        else resolve(r || { ok: false, error: "无响应" });
      });
    });
  }

  async function loadCfg() {
    const r = await sendBg({ cmd: "getState" });
    if (r.ok) cfg = r.settings;
    else cfg = { targetLang: "zh-CN", sourceLang: "auto", scenario: "general", displayStyle: "underline", batchSize: 8, concurrency: 4, selectionBubble: true };
    return cfg;
  }

  // ---------- helpers ----------
  function chunk(arr, n) {
    const out = [];
    for (let i = 0; i < arr.length; i += n) out.push(arr.slice(i, i + n));
    return out;
  }
  async function mapLimit(items, limit, fn) {
    let idx = 0;
    async function worker() {
      while (idx < items.length) {
        const cur = idx++;
        await fn(items[cur], cur);
      }
    }
    await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
  }
  const hasLetters = (s) => /[\p{L}]/u.test(s);
  function cjkRatio(s) {
    const letters = s.match(/[\p{L}]/gu) || [];
    if (!letters.length) return 0;
    const cjk = s.match(/[一-鿿぀-ヿ가-힯]/g) || [];
    return cjk.length / letters.length;
  }
  function shouldSkipText(text, target) {
    if (!hasLetters(text)) return true;
    if (text.trim().length < 2) return true;
    // Already in target script (rough heuristic for CJK targets).
    if (target && target.startsWith("zh") && cjkRatio(text) > 0.55) return true;
    if (target === "ja" && cjkRatio(text) > 0.7) return true;
    return false;
  }

  function isTranslatable(el) {
    if (!el || el.nodeType !== 1) return false;
    if (el.hasAttribute("data-prism-done")) return false;
    if (el.closest("[contenteditable=''], [contenteditable='true'], code, pre, .prism-translation")) return false;
    if (el.closest("#prism-popover, #prism-bubble, #prism-toast")) return false;
    const cs = getComputedStyle(el);
    if (cs.display === "none" || cs.visibility === "hidden") return false;
    return true;
  }

  function getText(el) {
    return (el.innerText || el.textContent || "").replace(/\s+/g, " ").trim();
  }

  // Only the element's OWN direct text nodes (not descendant blocks' text).
  function directText(el) {
    let s = "";
    for (const n of el.childNodes) if (n.nodeType === 3) s += n.nodeValue;
    return s.replace(/\s+/g, " ").trim();
  }

  function collectTargets(target) {
    const all = Array.from(document.body.querySelectorAll(BLOCK_SELECTOR));
    const out = [];
    for (const el of all) {
      if (out.length >= MAX_TARGETS) break;
      if (!isTranslatable(el)) continue;
      // If the block contains other block elements, it's a container: translate only
      // its own direct text (if any) so mixed content like "<li>Lead-in<ul>…</ul></li>"
      // isn't silently dropped. Pure containers (no own text) are skipped — their
      // child blocks are handled as their own targets.
      const hasChildBlock = !!el.querySelector(BLOCK_SELECTOR);
      const text = hasChildBlock ? directText(el) : getText(el);
      if (!text) continue;
      if (shouldSkipText(text, target)) continue;
      el._prismText = text;
      out.push(el);
    }
    return out;
  }

  // ---------- injection ----------
  function injectInto(el, style) {
    const tnode = document.createElement("span");
    tnode.className = "prism-translation prism-pending prism-style-" + style;
    const inner = document.createElement("span");
    inner.className = "prism-inner";
    inner.textContent = "翻译中";
    tnode.appendChild(inner);

    if (style === "replace") {
      const wrap = document.createElement("span");
      wrap.className = "prism-orig";
      wrap.style.display = "none";
      while (el.firstChild) wrap.appendChild(el.firstChild);
      el.appendChild(wrap);
    }
    el.appendChild(tnode);
    el.setAttribute("data-prism-done", "1");
    el._prismNode = inner;
    el._prismWrap = tnode;
    return inner;
  }

  // Wrap number(+unit) runs so they never break across lines (word-break:keep-all
  // alone does not prevent a break at a digit↔CJK boundary or at a space).
  function wrapNumbers(escaped) {
    return escaped.replace(
      /([$¥€£]?\d[\d,.:]*%?(?:\s?(?:亿|百万|千万|万|千|元|年|月|日|天|个|次|人|岁|km|kg|cm|mm|ms|GB|MB|KB|TB|m|g|s))?)/g,
      '<span class="prism-num">$1</span>'
    );
  }

  function fill(inner, text) {
    if (!inner) return;
    inner.innerHTML = wrapNumbers(escapeHtml(text));
    inner.parentElement?.classList.remove("prism-pending");
  }
  function fillError(inner, err) {
    if (!inner) return;
    inner.textContent = "⚠ " + (err || "翻译失败");
    inner.parentElement?.classList.remove("prism-pending");
    inner.parentElement?.classList.add("prism-error");
  }

  function restorePage() {
    document.querySelectorAll("[data-prism-done]").forEach((el) => {
      el.querySelectorAll(":scope > .prism-translation").forEach((n) => n.remove());
      const orig = el.querySelector(":scope > .prism-orig");
      if (orig) {
        while (orig.firstChild) el.insertBefore(orig.firstChild, orig);
        orig.remove();
      }
      el.removeAttribute("data-prism-done");
      delete el._prismNode;
      delete el._prismWrap;
    });
    pageActive = false;
    removeToast();
  }

  // ---------- toast ----------
  let toastEl = null;
  function showToast() {
    removeToast();
    toastEl = document.createElement("div");
    toastEl.id = "prism-toast";
    toastEl.innerHTML =
      '<span class="prism-spin"></span><span class="prism-msg">准备翻译…</span><span class="prism-bar"><i></i></span>';
    document.body.appendChild(toastEl);
  }
  function updateToast(done, total) {
    if (!toastEl) return;
    const pct = total ? Math.round((done / total) * 100) : 0;
    toastEl.querySelector(".prism-msg").textContent = `翻译中 ${done}/${total}`;
    toastEl.querySelector(".prism-bar > i").style.width = pct + "%";
  }
  function toastDone(msg) {
    if (!toastEl) return;
    toastEl.querySelector(".prism-spin")?.remove();
    toastEl.querySelector(".prism-msg").textContent = msg;
    toastEl.querySelector(".prism-bar > i").style.width = "100%";
    setTimeout(removeToast, 1800);
  }
  function removeToast() {
    toastEl?.remove();
    toastEl = null;
  }

  // ---------- page translate ----------
  let translating = false;
  async function translatePage() {
    if (translating) return;
    translating = true;
    await loadCfg();
    const targets = collectTargets(cfg.targetLang);
    if (!targets.length) {
      showToast();
      toastDone("未找到可翻译内容");
      translating = false;
      return;
    }
    showToast();
    pageActive = true;

    const inners = targets.map((el) => injectInto(el, cfg.displayStyle));
    const texts = targets.map((el) => el._prismText);
    const indexed = texts.map((t, i) => ({ t, i }));
    const batches = chunk(indexed, cfg.batchSize || 8);

    let done = 0;
    updateToast(0, texts.length);
    await mapLimit(batches, cfg.concurrency || 4, async (batch) => {
      const payload = batch.map((b) => b.t);
      const resp = await sendBg({
        cmd: "translateBatch",
        texts: payload,
        targetLang: cfg.targetLang,
        sourceLang: cfg.sourceLang,
        scenario: cfg.scenario,
      });
      if (resp.ok) {
        resp.translations.forEach((tr, j) => fill(inners[batch[j].i], tr));
      } else {
        batch.forEach((b) => fillError(inners[b.i], resp.error));
      }
      done += batch.length;
      updateToast(done, texts.length);
    });
    toastDone("翻译完成 ✓");
    translating = false;
  }

  // ---------- selection bubble + popover ----------
  let bubble = null;
  let popover = null;

  function removeBubble() { bubble?.remove(); bubble = null; }
  function removePopover() { popover?.remove(); popover = null; }

  function inEditable() {
    const a = document.activeElement;
    return a && (a.tagName === "INPUT" || a.tagName === "TEXTAREA" || a.isContentEditable);
  }

  document.addEventListener("mouseup", (e) => {
    if (e.target.closest && e.target.closest("#prism-bubble, #prism-popover")) return;
    setTimeout(() => {
      if (!cfg) return;
      if (cfg.selectionBubble === false) return;
      const sel = window.getSelection();
      const text = sel ? sel.toString().trim() : "";
      if (!text || text.length < 1 || inEditable() || !hasLetters(text) || !sel.rangeCount) {
        removeBubble();
        return;
      }
      const range = sel.getRangeAt(0);
      const rect = range.getBoundingClientRect();
      showBubble(rect, text);
    }, 10);
  });

  document.addEventListener("mousedown", (e) => {
    if (e.target.closest && e.target.closest("#prism-bubble, #prism-popover")) return;
    removeBubble();
    removePopover();
  });

  // Dismiss floating UI on scroll/resize so it never orphans far from the selection.
  const dismissFloat = () => { removeBubble(); removePopover(); };
  window.addEventListener("scroll", dismissFloat, { passive: true, capture: true });
  window.addEventListener("resize", dismissFloat);

  function showBubble(rect, text) {
    removeBubble();
    bubble = document.createElement("div");
    bubble.id = "prism-bubble";
    bubble.innerHTML = '<span class="prism-logo"></span><span>翻译</span>';
    bubble.style.top = rect.bottom + window.scrollY + 8 + "px";
    bubble.style.left = rect.left + rect.width / 2 + window.scrollX + "px";
    bubble.addEventListener("click", (ev) => {
      ev.stopPropagation();
      translateSelection(text, rect);
    });
    document.body.appendChild(bubble);
    // clamp the center-anchored (translateX(-50%)) bubble inside the viewport
    const half = bubble.getBoundingClientRect().width / 2;
    let center = rect.left + rect.width / 2;
    center = Math.max(half + 6, Math.min(window.innerWidth - half - 6, center));
    bubble.style.left = center + window.scrollX + "px";
  }

  async function translateSelection(text, rect) {
    removeBubble();
    showPopover(rect, '<div class="prism-pop-loading">翻译中…</div>', text);
    await loadCfg();
    const resp = await sendBg({
      cmd: "translateOne",
      text,
      targetLang: cfg.targetLang,
      sourceLang: cfg.sourceLang,
      scenario: cfg.scenario,
    });
    if (resp.ok) {
      const body =
        `<div class="prism-pop-src">${escapeHtml(text.slice(0, 200))}</div>` +
        `<div class="prism-pop-body">${escapeHtml(resp.text)}</div>` +
        `<div class="prism-pop-meta"><span>${escapeHtml(resp.provider || "")}</span><span class="prism-copy" style="cursor:pointer">点击复制译文</span></div>`;
      showPopover(rect, body, text, resp.text);
    } else {
      showPopover(rect, `<div style="color:#ef4444">翻译失败：${escapeHtml(resp.error)}</div>`, text);
    }
  }

  function showPopover(rect, html, srcText, transText) {
    removePopover();
    popover = document.createElement("div");
    popover.id = "prism-popover";
    popover.innerHTML = html;
    const top = rect.bottom + window.scrollY + 8;
    let left = rect.left + window.scrollX;
    popover.style.top = top + "px";
    popover.style.left = left + "px";
    document.body.appendChild(popover);
    // clamp to viewport width
    const pr = popover.getBoundingClientRect();
    if (pr.right > window.innerWidth - 12) {
      popover.style.left = Math.max(8, window.innerWidth - pr.width - 12 + window.scrollX) + "px";
    }
    const copy = popover.querySelector(".prism-copy");
    if (copy && transText) {
      copy.addEventListener("click", () => {
        navigator.clipboard.writeText(transText)
          .then(() => { copy.textContent = "已复制 ✓"; })
          .catch(() => { copy.textContent = "复制失败"; });
      });
    }
  }

  function escapeHtml(s) {
    return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
  }

  // ---------- command router ----------
  chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
    (async () => {
      if (msg.cmd === "translate") {
        await translatePage();
        sendResponse({ ok: true, active: true });
      } else if (msg.cmd === "restore") {
        restorePage();
        sendResponse({ ok: true, active: false });
      } else if (msg.cmd === "toggle") {
        if (pageActive) restorePage();
        else await translatePage();
        sendResponse({ ok: true, active: pageActive });
      } else if (msg.cmd === "translateSelection") {
        const sel = window.getSelection();
        let rect = { left: window.innerWidth / 2, top: 80, bottom: 80, width: 0, right: window.innerWidth / 2 };
        if (sel && sel.rangeCount) rect = sel.getRangeAt(0).getBoundingClientRect();
        translateSelection(msg.text || (sel ? sel.toString() : ""), rect);
        sendResponse({ ok: true });
      } else if (msg.cmd === "ping") {
        sendResponse({ ok: true, active: pageActive });
      }
    })();
    return true;
  });

  // ---------- init ----------
  loadCfg().then(() => {
    if (cfg && cfg.triggerMode === "auto") {
      // small delay to let SPA content settle
      setTimeout(() => translatePage(), 800);
    }
  });
})();
