(() => {
  "use strict";

  const sleep = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
  const textOf = node => String(node?.innerText || node?.textContent || "").trim();
  let activeTranslationProbe = null;
  const cancelledTaskIds = new Set();

  function taskCancelledError() {
    const error = new Error("编辑页面已关闭，翻译任务已终止");
    error.code = "EDITOR_TASK_CANCELLED";
    return error;
  }

  function assertTaskActive(taskId) {
    if (cancelledTaskIds.has(taskId)) throw taskCancelledError();
  }
  const FIELD_DEFS = [
    { number: 1, key: "title", label: "产品名称" },
    { number: 2, key: "productHighlight", label: "产品亮点" },
    { number: 3, key: "keywords", label: "关键词" },
    { number: 4, key: "bullet0", label: "产品五点1" },
    { number: 5, key: "bullet1", label: "产品五点2" },
    { number: 6, key: "bullet2", label: "产品五点3" },
    { number: 7, key: "bullet3", label: "产品五点4" },
    { number: 8, key: "bullet4", label: "产品五点5" },
    { number: 9, key: "description", label: "产品描述" }
  ];

  function entriesFor(content = {}) {
    const bullets = Array.isArray(content.bulletPoints) ? content.bulletPoints : [];
    const values = {
      title: content.title,
      productHighlight: content.productHighlight,
      keywords: content.keywords,
      bullet0: bullets[0], bullet1: bullets[1], bullet2: bullets[2], bullet3: bullets[3], bullet4: bullets[4],
      description: content.description
    };
    return FIELD_DEFS.map(def => ({ ...def, text: String(values[def.key] || "").trim() }))
      .filter(entry => entry.text);
  }

  function packBatches(entries, maxLength = 4200) {
    const batches = [];
    let batch = [];
    let length = 0;
    entries.forEach(entry => {
      const blockLength = String(entry.number).length + entry.text.length + 8;
      if (batch.length && length + blockLength > maxLength) {
        batches.push(batch);
        batch = [];
        length = 0;
      }
      batch.push(entry);
      length += blockLength;
    });
    if (batch.length) batches.push(batch);
    return batches;
  }

  const batchText = batch => batch.map(entry => `${entry.number}\n${entry.text}`).join("\n\n");

  function visible(node) {
    const rect = node.getBoundingClientRect();
    const style = getComputedStyle(node);
    return rect.width > 120 && rect.height > 30 && style.display !== "none" && style.visibility !== "hidden";
  }

  function findSourceInput() {
    const selectors = [
      'textarea[aria-label*="Source text"]', 'textarea[aria-label*="源文本"]',
      'textarea[aria-label*="输入"]', "textarea",
      '[contenteditable="true"][role="textbox"]', '[contenteditable="true"]'
    ];
    for (const selector of selectors) {
      const node = [...document.querySelectorAll(selector)].find(visible);
      if (node) return node;
    }
    return null;
  }

  function setInputValue(input, value) {
    input.focus();
    input.dispatchEvent(new FocusEvent("focus", { bubbles: true }));
    if (input instanceof HTMLTextAreaElement || input instanceof HTMLInputElement) {
      const prototype = input instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype;
      const setter = Object.getOwnPropertyDescriptor(prototype, "value")?.set;
      if (setter) setter.call(input, value);
      else input.value = value;
    } else {
      input.textContent = value;
    }
    input.dispatchEvent(new InputEvent("beforeinput", { bubbles: true, inputType: "insertText", data: value }));
    input.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: value }));
    input.dispatchEvent(new Event("change", { bubbles: true }));
  }

  const LABEL_ALIASES = {
    "产品名称": ["产品名称", "品名", "Product Name", "Product name", "product name", "Title", "title"],
    "产品亮点": ["产品亮点", "产品卖点", "亮点", "卖点", "Product Highlight", "product highlight", "Highlight", "highlight"],
    "关键词": ["关键词", "关键字", "Keywords", "keywords", "Keyword", "keyword"],
    "产品五点1": ["产品五点1", "产品卖点1", "产品五点 1", "产品卖点 1", "Bullet Point 1", "bullet point 1", "Bullet 1", "bullet 1"],
    "产品五点2": ["产品五点2", "产品卖点2", "产品五点 2", "产品卖点 2", "Bullet Point 2", "bullet point 2", "Bullet 2", "bullet 2"],
    "产品五点3": ["产品五点3", "产品卖点3", "产品五点 3", "产品卖点 3", "Bullet Point 3", "bullet point 3", "Bullet 3", "bullet 3"],
    "产品五点4": ["产品五点4", "产品卖点4", "产品五点 4", "产品卖点 4", "Bullet Point 4", "bullet point 4", "Bullet 4", "bullet 4"],
    "产品五点5": ["产品五点5", "产品卖点5", "产品五点 5", "产品卖点 5", "Bullet Point 5", "bullet point 5", "Bullet 5", "bullet 5"],
    "产品描述": ["产品描述", "描述", "Product Description", "product description", "Description", "description"]
  };

  function normalizeLabelText(value) {
    let text = String(value || "")
      .replace(/[［【]\s*/g, "【")
      .replace(/\s*[］】]/g, "】")
      .replace(/【\s*产品卖点\s*([1-5])\s*】/g, "【产品五点$1】");
    Object.entries(LABEL_ALIASES).forEach(([standard, aliases]) => {
      aliases.forEach(alias => {
        const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
        text = text.replace(new RegExp(`(^|\\n)\\s*${escaped}\\s*[：:]\\s*`, "gi"), `$1【${standard}】\n`);
      });
    });
    return text.replace(/【\s*(产品名称|产品亮点|关键词|产品五点[1-5]|产品描述)\s*】/g, "【$1】");
  }

  function translatedCandidates(batch) {
    const labels = batch.map(entry => `【${entry.label}】`);
    const labelPatterns = batch.flatMap(entry =>
      (LABEL_ALIASES[entry.label] || [entry.label]).flatMap(alias => [`【${alias}】`, `${alias}：`, `${alias}:`])
    );
    const selectors = [
      'div[data-language-for-alternatives] span', 'span[jsname="W297wb"]',
      ".ryNqvb", ".J0lOec", ".tlid-translation", '[data-testid*="translation"]'
    ];
    const input = findSourceInput();
    const sourceText = input ? (input.value || input.innerText || input.textContent || "") : "";
    const candidates = [];
    const allValues = [];
    const validTranslatedValue = raw => {
      if (/^(clear|search|close|checkhistory|auto_awesome|error_outline)$/i.test(raw)) return "";
      if (/语音|手写|键盘|加载中|请稍候|了解详情|替代译文|检测语言|交换源语言|关闭选择器/i.test(raw)) return "";
      const value = normalizeLabelText(raw);
      return !value || value.length < 2 || value === sourceText ? "" : value;
    };
    selectors.forEach(selector => {
      const values = [];
      document.querySelectorAll(selector).forEach(node => {
        if (node.isContentEditable || /^(TEXTAREA|INPUT)$/.test(node.tagName || "")) return;
        if (node.closest?.('[contenteditable="true"], textarea, button, a, select, option, [role="button"], [role="menu"]')) return;
        const value = validTranslatedValue(textOf(node));
        if (!value || value.length < 10) return;
        if (!labelPatterns.some(pattern => value.includes(pattern))) return;
        candidates.push(value);
        values.push(value);
        allValues.push(value);
      });
      const combined = normalizeLabelText(values.join("\n"));
      if (combined && combined !== sourceText && labelPatterns.filter(pattern => combined.includes(pattern)).length >= Math.ceil(batch.length / 2)) {
        candidates.push(combined);
      }
    });
    // Google 会把一个段落按英文句号拆成多个 ryNqvb 节点，只有第一个节点
    // 带“【字段名】”。必须从完整结果容器和连续兄弟节点收集，否则句号后的内容会丢失。
    document.querySelectorAll('[data-language-for-alternatives]').forEach(container => {
      const value = validTranslatedValue(textOf(container));
      if (value && labelPatterns.some(pattern => value.includes(pattern))) candidates.push(value);
    });
    const orderedValues = [...document.querySelectorAll('.ryNqvb, [jsname="W297wb"]')]
      .filter(node => !node.closest?.('textarea, input, [contenteditable="true"], button, a, select, option'))
      .map(node => validTranslatedValue(textOf(node)))
      .filter(Boolean);
    orderedValues.forEach((value, index) => {
      if (!labelPatterns.some(pattern => value.includes(pattern))) return;
      const sequence = [value];
      for (let cursor = index + 1; cursor < orderedValues.length; cursor++) {
        if (labelPatterns.some(pattern => orderedValues[cursor].includes(pattern))) break;
        sequence.push(orderedValues[cursor]);
      }
      candidates.push(normalizeLabelText(sequence.join("\n")));
    });
    const orderedCombined = normalizeLabelText(orderedValues.join("\n"));
    if (orderedCombined && labelPatterns.some(pattern => orderedCombined.includes(pattern))) {
      candidates.push(orderedCombined);
    }
    const explicitValues = [...document.querySelectorAll('.ryNqvb, [jsname="W297wb"], [data-language-for-alternatives]')]
      .filter(node => !node.closest?.('textarea, input, [contenteditable="true"]'))
      .map(node => normalizeLabelText(textOf(node)))
      .filter(value => value && value !== sourceText && labelPatterns.some(pattern => value.includes(pattern)));
    const explicitCombined = normalizeLabelText(explicitValues.join("\n"));
    if (explicitCombined && labels.every(label => explicitCombined.includes(label))) {
      candidates.push(explicitCombined);
    }
    const globalCombined = normalizeLabelText(allValues.join("\n"));
    if (globalCombined && globalCombined !== sourceText && labels.some(label => globalCombined.includes(label))) {
      candidates.push(globalCombined);
    }
    return [...new Set(candidates)].sort((a, b) => {
      const score = value => labelPatterns.filter(pattern => value.includes(pattern)).length;
      const chinese = value => (value.match(/[\u4e00-\u9fff]/g) || []).length;
      return score(b) - score(a) || chinese(b) - chinese(a) || b.length - a.length;
    });
  }

  function waitForTranslation(batch) {
    const labels = batch.map(entry => `【${entry.label}】`);
    const labelVariants = batch.flatMap(entry =>
      (LABEL_ALIASES[entry.label] || [entry.label]).flatMap(alias => [`【${alias}】`, `${alias}：`, `${alias}:`])
    );
    return new Promise((resolve, reject) => {
      let previous = "";
      let stableSince = 0;
      let settled = false;
      let stableTimer = null;
      const cleanup = () => {
        clearInterval(interval);
        clearTimeout(timeout);
        clearTimeout(stableTimer);
        observer?.disconnect();
        if (activeTranslationProbe?.check === check) activeTranslationProbe = null;
      };
      const check = () => {
        if (settled) return;
        const candidates = translatedCandidates(batch);
        const current = candidates
          .filter(value => labels.every(label => value.includes(label)))
          .sort((a, b) => b.length - a.length)[0]
          || candidates.find(value => labelVariants.filter(label => value.includes(label)).length >= Math.ceil(batch.length / 2))
          || candidates.find(value => value.length > 50 && /[\u4e00-\u9fff]/.test(value))
          || "";
        if (!current) return;
        if (current === previous && Date.now() - stableSince >= 500) {
          settled = true;
          cleanup();
          resolve(current);
          return;
        }
        if (current !== previous) {
          previous = current;
          stableSince = Date.now();
          clearTimeout(stableTimer);
          stableTimer = setTimeout(check, 550);
        }
      };
      let observer = null;
      try {
        observer = new MutationObserver(check);
        observer.observe(document.body, { childList: true, subtree: true, characterData: true });
      } catch {}
      const interval = setInterval(check, 300);
      const timeout = setTimeout(() => {
        if (settled) return;
        settled = true;
        cleanup();
        reject(new Error("翻译页面已打开，但没有识别到完整译文；请检查该翻译网址是否支持文本翻译"));
      }, 90000);
      activeTranslationProbe = { check };
      check();
    });
  }

  function plainTranslationCandidates(source = "") {
    const values = [];
    const sourceLen = String(source || "").length;
    const selectors = [
      'div[data-language-for-alternatives]', 'span[jsname="W297wb"]',
      ".ryNqvb", ".J0lOec", ".tlid-translation", '[data-testid*="translation"]'
    ];
    selectors.forEach(selector => document.querySelectorAll(selector).forEach(node => {
      if (node.closest?.('textarea, input, [contenteditable="true"], button, a, select, option')) return;
      const value = textOf(node);
      if (!value || value === source || /^(clear|search|close|checkhistory|auto_awesome)$/i.test(value)) return;
      if (/语音|手写|键盘|加载中|请稍候|替代译文|检测语言|交换源语言/i.test(value)) return;
      values.push(value);
    }));
    return [...new Set(values)].sort((left, right) => {
      const zh = value => (value.match(/[\u4e00-\u9fff]/g) || []).length;
      const zhLeft = zh(left), zhRight = zh(right);
      // 优先选择有中文且长度接近原文的结果，避免音译拼音覆盖正常译文
      if (zhLeft > 0 && zhRight === 0) return -1;
      if (zhRight > 0 && zhLeft === 0) return 1;
      const lenScore = value => Math.abs(value.length - sourceLen);
      return lenScore(left) - lenScore(right) || zhRight - zhLeft || right.length - left.length;
    });
  }

  function waitForPlainTranslation(source, staleValues = new Set()) {
    return new Promise((resolve, reject) => {
      let previous = "", stableSince = 0, settled = false, observer = null, stableTimer = null;
      const cleanup = () => { clearInterval(interval); clearTimeout(timeout); clearTimeout(stableTimer); observer?.disconnect(); if (activeTranslationProbe?.check === check) activeTranslationProbe = null; };
      const check = () => {
        if (settled) return;
        const current = plainTranslationCandidates(source).find(value => !staleValues.has(value)) || "";
        if (!current) return;
        if (current === previous && Date.now() - stableSince >= 500) {
          settled = true; cleanup(); resolve(current); return;
        }
        if (current !== previous) {
          previous = current;
          stableSince = Date.now();
          clearTimeout(stableTimer);
          stableTimer = setTimeout(check, 550);
        }
      };
      try { observer = new MutationObserver(check); observer.observe(document.body, { childList: true, subtree: true, characterData: true }); } catch {}
      const interval = setInterval(check, 300);
      const timeout = setTimeout(() => { if (!settled) { settled = true; cleanup(); reject(new Error("翻译页面未返回完整字段译文")); } }, 90000);
      activeTranslationProbe = { check };
      check();
    });
  }

  function parseBatch(value, batch) {
    const normalized = normalizeLabelText(value);
    const result = {};
    const positions = batch.map(entry => {
      const label = `【${entry.label}】`;
      return { entry, label, pos: normalized.indexOf(label) };
    }).filter(item => item.pos >= 0).sort((a, b) => a.pos - b.pos);
    positions.forEach((item, index) => {
      const start = item.pos + item.label.length;
      const end = positions[index + 1]?.pos ?? normalized.length;
      result[item.entry.key] = normalized.slice(start, end).trim();
    });
    if (!positions.length) {
      const lines = normalized.split(/\n+/).map(line => line.trim()).filter(Boolean);
      if (lines.length >= batch.length) batch.forEach((entry, index) => { result[entry.key] = lines[index] || ""; });
    }
    return result;
  }

  function parseNumberedBatch(value, batch) {
    const expected = new Map(batch.map(entry => [String(entry.number), entry]));
    const normalized = String(value || "").replace(/\r\n?/g, "\n");
    // A field boundary must be a line containing only one digit. Product
    // descriptions commonly contain numbered lists such as "1. Material";
    // accepting a digit at the start of those lines shifts every later field.
    const marker = /(?:^|\n)[ \t]*([1-9])[ \t]*(?=\n|$)/g;
    const positions = [];
    let match;
    while ((match = marker.exec(normalized))) {
      const entry = expected.get(match[1]);
      if (entry) positions.push({ entry, start: match.index, contentStart: marker.lastIndex });
    }
    const result = {};
    positions.forEach((item, index) => {
      const end = positions[index + 1]?.start ?? normalized.length;
      result[item.entry.key] = normalized.slice(item.contentStart, end).trim();
    });
    return result;
  }

  function numberedTranslationCandidates(source, batch) {
    const expectedKeys = new Set(batch.map(entry => entry.key));
    const values = [];
    const selectors = [
      '[data-language-for-alternatives]', 'span[jsname="W297wb"]',
      ".ryNqvb", ".J0lOec", ".tlid-translation", '[data-testid*="translation"]'
    ];
    const usable = node => !node.closest?.('textarea, input, [contenteditable="true"], button, a, select, option');
    selectors.forEach(selector => document.querySelectorAll(selector).forEach(node => {
      if (!usable(node)) return;
      const value = textOf(node);
      if (value && value !== source) values.push(value);
    }));
    const ordered = [...document.querySelectorAll('.ryNqvb, [jsname="W297wb"]')]
      .filter(usable).map(textOf).filter(Boolean);
    if (ordered.length) values.push(ordered.join("\n"));
    return [...new Set(values)].filter(value => {
      const parsed = parseNumberedBatch(value, batch);
      return [...expectedKeys].every(key => String(parsed[key] || "").trim());
    }).sort((left, right) => right.length - left.length);
  }

  function waitForNumberedTranslation(source, batch, staleValues = new Set(), taskId = "") {
    return new Promise((resolve, reject) => {
      let previous = "", stableSince = 0, settled = false, observer = null, stableTimer = null;
      const cleanup = () => {
        clearInterval(interval); clearTimeout(timeout); clearTimeout(stableTimer); observer?.disconnect();
        if (activeTranslationProbe?.check === check) activeTranslationProbe = null;
      };
      const check = () => {
        if (settled) return;
        if (cancelledTaskIds.has(taskId)) {
          settled = true; cleanup(); reject(taskCancelledError()); return;
        }
        const current = numberedTranslationCandidates(source, batch).find(value => !staleValues.has(value)) || "";
        if (!current) return;
        if (current === previous && Date.now() - stableSince >= 650) {
          settled = true; cleanup(); resolve(current); return;
        }
        if (current !== previous) {
          previous = current; stableSince = Date.now();
          clearTimeout(stableTimer); stableTimer = setTimeout(check, 700);
        }
      };
      try { observer = new MutationObserver(check); observer.observe(document.body, { childList: true, subtree: true, characterData: true }); } catch {}
      const interval = setInterval(check, 300);
      const timeout = setTimeout(() => {
        if (!settled) {
          settled = true; cleanup();
          reject(new Error("翻译页面未返回完整的编号字段译文，请保持翻译页打开后重试"));
        }
      }, 90000);
      const cancel = () => {
        if (settled) return;
        settled = true; cleanup(); reject(taskCancelledError());
      };
      activeTranslationProbe = { taskId, check, cancel };
      check();
    });
  }

  function resultFrom(pieces) {
    return {
      title: pieces.title || "",
      productHighlight: pieces.productHighlight || "",
      keywords: pieces.keywords || "",
      bulletPoints: [0, 1, 2, 3, 4].map(index => pieces[`bullet${index}`] || ""),
      description: pieces.description || ""
    };
  }

  async function finish(task, payload) {
    if (cancelledTaskIds.has(task.id)) return;
    const { activeGoogleTask } = await chrome.storage.local.get("activeGoogleTask");
    if (activeGoogleTask?.id !== task.id) return;
    const message = {
      type: "GOOGLE_TRANSLATE_RESULT",
      taskId: task.id,
      returnTabId: task.returnTabId,
      ...payload
    };
    await chrome.storage.local.set({ lastGoogleTranslateResult: message });
    await chrome.storage.local.remove("activeGoogleTask");
    await chrome.runtime.sendMessage(message);
  }

  chrome.runtime.onMessage?.addListener((message, _sender, sendResponse) => {
    if (message?.type === "CANCEL_GOOGLE_TRANSLATION") {
      if (message.taskId) cancelledTaskIds.add(message.taskId);
      if (activeTranslationProbe?.taskId === message.taskId) activeTranslationProbe.cancel?.();
      sendResponse?.({ ok: true, cancelled: true });
      return false;
    }
    if (message?.type !== "WAKE_GOOGLE_TRANSLATION") return;
    if (!message.taskId || window.__wzcTranslationTask === message.taskId) activeTranslationProbe?.check();
    sendResponse?.({ ok: true, running: Boolean(activeTranslationProbe) });
  });

  async function run(task) {
    assertTaskActive(task.id);
    const entries = entriesFor(task.content);
    let input = null;
    for (let attempt = 0; attempt < 100; attempt++) {
      assertTaskActive(task.id);
      input = findSourceInput();
      if (input) break;
      await sleep(300);
    }
    if (!input) throw new Error("没有找到翻译软件的文本输入框");

    const pieces = {};
    for (const batch of packBatches(entries)) {
      assertTaskActive(task.id);
      const source = batchText(batch);
      const staleValues = new Set(numberedTranslationCandidates("", batch));
      try { await navigator.clipboard.writeText(source); } catch {}
      setInputValue(input, source);
      // Allow one short render turn before reading the translated result.
      // Result stability is still checked below, so a long pre-send pause is unnecessary.
      await sleep(30);
      Object.assign(pieces, parseNumberedBatch(
        await waitForNumberedTranslation(source, batch, staleValues, task.id), batch
      ));
      await sleep(100);
    }
    await finish(task, { ok: true, result: resultFrom(pieces) });
  }

  async function boot() {
    const { activeGoogleTask: task } = await chrome.storage.local.get("activeGoogleTask");
    if (!task || window.__wzcTranslationTask === task.id) return;
    if (task.translationTabId && task.translationTabId !== undefined) {
      try {
        const currentTab = await chrome.runtime.sendMessage({ type: "GET_CURRENT_TAB_ID" });
        if (currentTab?.tabId && currentTab.tabId !== task.translationTabId) return;
      } catch {}
    }
    window.__wzcTranslationTask = task.id;
    try {
      await run(task);
    } catch (error) {
      if (error?.code === "EDITOR_TASK_CANCELLED") return;
      await finish(task, { ok: false, error: error.message || "翻译自动回填失败" });
    }
  }

  boot();
})();
