/**
 * cdp_client.js
 * Chrome DevTools Protocol wrapper for browser automation.
 *
 * Runs in the extension service worker and exposes a compact browser.* tool
 * surface through BROWSER_* runtime messages.
 */
(function () {
  'use strict';

  const CDP_VERSION = '1.3';
  const attachedTabs = new Set();

  function chromeCall(api, ...args) {
    return new Promise((resolve, reject) => {
      api(...args, (result) => {
        const err = chrome.runtime.lastError;
        if (err) reject(new Error(err.message));
        else resolve(result);
      });
    });
  }

  function normalizeUrl(rawUrl = '') {
    const value = String(rawUrl || '').trim();
    if (!/^https?:\/\//i.test(value)) {
      throw new Error('URL 必须以 http:// 或 https:// 开头');
    }
    return new URL(value).toString();
  }

  function isBlockedUrl(url = '') {
    return /^(chrome|chrome-extension|edge|about|devtools):/i.test(String(url || ''));
  }

  async function getTab(tabId) {
    if (!tabId) throw new Error('缺少 tabId');
    return await chromeCall(chrome.tabs.get, tabId);
  }

  async function getTargetTab(tabId) {
    if (tabId) return await getTab(tabId);
    if (globalThis._automationTabId) return await getTab(globalThis._automationTabId);
    const tabs = await chromeCall(chrome.tabs.query, { active: true, currentWindow: true });
    const tab = tabs?.[0];
    if (!tab?.id) throw new Error('未找到可操作的 Tab');
    return tab;
  }

  async function ensureAttached(tabId) {
    const tab = await getTab(tabId);
    if (isBlockedUrl(tab.url)) throw new Error('浏览器内部页面不支持自动化操作');
    if (!attachedTabs.has(tabId)) {
      try {
        await chromeCall(chrome.debugger.attach, { tabId }, CDP_VERSION);
      } catch (e) {
        if (!/already attached/i.test(e.message || '')) throw e;
      }
      attachedTabs.add(tabId);
    }
    await sendCommand(tabId, 'Runtime.enable').catch(() => {});
    await sendCommand(tabId, 'Page.enable').catch(() => {});
    await sendCommand(tabId, 'DOM.enable').catch(() => {});
    await sendCommand(tabId, 'Network.enable').catch(() => {});
  }

  async function detach(tabId) {
    if (!tabId || !attachedTabs.has(tabId)) return { detached: false };
    try {
      await chromeCall(chrome.debugger.detach, { tabId });
    } finally {
      attachedTabs.delete(tabId);
    }
    return { detached: true, tabId };
  }

  async function sendCommand(tabId, method, params = {}) {
    return await chromeCall(chrome.debugger.sendCommand, { tabId }, method, params);
  }

  async function evaluate(tabId, expression) {
    await ensureAttached(tabId);
    const result = await sendCommand(tabId, 'Runtime.evaluate', {
      expression,
      awaitPromise: true,
      returnByValue: true,
      userGesture: true
    });
    if (result.exceptionDetails) {
      const message = result.exceptionDetails.exception?.description ||
        result.exceptionDetails.text ||
        '页面脚本执行失败';
      throw new Error(message);
    }
    return result.result?.value;
  }

  async function waitForTabLoad(tabId, timeout = 15000) {
    const current = await getTab(tabId).catch(() => null);
    if (current?.status === 'complete') return current;
    return await new Promise((resolve, reject) => {
      let done = false;
      const timer = setTimeout(async () => {
        if (done) return;
        done = true;
        chrome.tabs.onUpdated.removeListener(listener);
        try {
          resolve(await getTab(tabId));
        } catch (e) {
          reject(e);
        }
      }, timeout);
      const listener = (updatedTabId, changeInfo, tab) => {
        if (updatedTabId !== tabId) return;
        if (changeInfo.status !== 'complete') return;
        if (done) return;
        done = true;
        clearTimeout(timer);
        chrome.tabs.onUpdated.removeListener(listener);
        resolve(tab);
      };
      chrome.tabs.onUpdated.addListener(listener);
    });
  }

  async function open(args = {}) {
    const url = normalizeUrl(args.url);
    const newTab = args.newTab !== false;
    let tab;
    if (newTab || !globalThis._automationTabId) {
      tab = await chromeCall(chrome.tabs.create, { url, active: args.active !== false });
    } else {
      tab = await chromeCall(chrome.tabs.update, globalThis._automationTabId, { url, active: args.active !== false });
    }
    globalThis._automationTabId = tab.id;
    const loaded = await waitForTabLoad(tab.id, args.timeout || 20000);
    await ensureAttached(loaded.id);
    const summary = await getText({ tabId: loaded.id, maxLength: 2000 }).catch(() => ({}));
    return {
      tabId: loaded.id,
      url: loaded.url || url,
      title: loaded.title || summary.title || '',
      text: summary.text || ''
    };
  }

  async function navigate(args = {}) {
    const tab = await getTargetTab(args.tabId);
    const url = normalizeUrl(args.url);
    await chromeCall(chrome.tabs.update, tab.id, { url, active: args.active !== false });
    const loaded = await waitForTabLoad(tab.id, args.timeout || 20000);
    globalThis._automationTabId = loaded.id;
    await ensureAttached(loaded.id);
    return { tabId: loaded.id, url: loaded.url || url, title: loaded.title || '' };
  }

  function elementLocatorExpression(args = {}) {
    if (args.selector) {
      const missingMessage = JSON.stringify('元素未找到: ' + args.selector);
      return `
(() => {
  const el = document.querySelector(${JSON.stringify(args.selector)});
  if (!el) return { ok: false, error: ${missingMessage} };
  el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
  const rect = el.getBoundingClientRect();
  return {
    ok: true,
    x: rect.left + rect.width / 2,
    y: rect.top + rect.height / 2,
    tagName: el.tagName,
    text: (el.innerText || el.textContent || el.value || '').slice(0, 200)
  };
})()`;
    }
    if (args.text) {
      return `
(() => {
  const needle = ${JSON.stringify(String(args.text || '').trim())};
  const visible = (el) => {
    const s = getComputedStyle(el);
    const r = el.getBoundingClientRect();
    return s.display !== 'none' && s.visibility !== 'hidden' && r.width > 1 && r.height > 1;
  };
  const candidates = Array.from(document.querySelectorAll('button,a,input,textarea,select,[role="button"],[contenteditable="true"],label,[onclick],span,div'))
    .filter(visible);
  const el = candidates.find(item => ((item.innerText || item.textContent || item.value || '').trim()).includes(needle));
  if (!el) return { ok: false, error: '未找到包含文本的元素: ' + needle };
  el.scrollIntoView({ block: 'center', inline: 'center', behavior: 'instant' });
  const rect = el.getBoundingClientRect();
  return {
    ok: true,
    x: rect.left + rect.width / 2,
    y: rect.top + rect.height / 2,
    tagName: el.tagName,
    text: (el.innerText || el.textContent || el.value || '').slice(0, 200)
  };
})()`;
    }
    return null;
  }

  async function click(args = {}) {
    const tab = await getTargetTab(args.tabId);
    await ensureAttached(tab.id);
    let point = null;
    if (Number.isFinite(Number(args.x)) && Number.isFinite(Number(args.y))) {
      point = { ok: true, x: Number(args.x), y: Number(args.y), text: '坐标点击' };
    } else {
      const expr = elementLocatorExpression(args);
      if (!expr) throw new Error('请提供 selector、text 或 x/y 坐标');
      point = await evaluate(tab.id, expr);
    }
    if (!point?.ok) throw new Error(point?.error || '未找到可点击目标');
    const button = args.button || 'left';
    const clickCount = args.clickCount || 1;
    await sendCommand(tab.id, 'Input.dispatchMouseEvent', { type: 'mouseMoved', x: point.x, y: point.y, button: 'none' });
    await sendCommand(tab.id, 'Input.dispatchMouseEvent', { type: 'mousePressed', x: point.x, y: point.y, button, clickCount });
    await sendCommand(tab.id, 'Input.dispatchMouseEvent', { type: 'mouseReleased', x: point.x, y: point.y, button, clickCount });
    if (args.waitAfter !== false) await new Promise(r => setTimeout(r, Number(args.waitAfter || 600)));
    return { clicked: true, tabId: tab.id, target: point };
  }

  async function type(args = {}) {
    const tab = await getTargetTab(args.tabId);
    await ensureAttached(tab.id);
    if (args.selector || args.textTarget) {
      await click({ tabId: tab.id, selector: args.selector, text: args.textTarget, waitAfter: 100 });
    }
    if (args.clear !== false) {
      await evaluate(tab.id, `
(() => {
  const el = document.activeElement;
  if (!el) return false;
  if ('value' in el) {
    el.value = '';
    el.dispatchEvent(new Event('input', { bubbles: true }));
    el.dispatchEvent(new Event('change', { bubbles: true }));
    return true;
  }
  if (el.isContentEditable) {
    el.textContent = '';
    el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'deleteContentBackward' }));
    return true;
  }
  return false;
})()`).catch(() => {});
    }
    await sendCommand(tab.id, 'Input.insertText', { text: String(args.value ?? args.text ?? '') });
    if (args.waitAfter !== false) await new Promise(r => setTimeout(r, Number(args.waitAfter || 300)));
    return { typed: true, tabId: tab.id, length: String(args.value ?? args.text ?? '').length };
  }

  async function press(args = {}) {
    const tab = await getTargetTab(args.tabId);
    await ensureAttached(tab.id);
    const key = args.key || 'Enter';
    const modifiers = Number(args.modifiers || 0);
    await sendCommand(tab.id, 'Input.dispatchKeyEvent', { type: 'keyDown', key, code: args.code || key, modifiers });
    await sendCommand(tab.id, 'Input.dispatchKeyEvent', { type: 'keyUp', key, code: args.code || key, modifiers });
    return { pressed: key, tabId: tab.id };
  }

  async function waitFor(args = {}) {
    const tab = await getTargetTab(args.tabId);
    const selector = args.selector;
    if (!selector) throw new Error('缺少 selector');
    const timeout = Number(args.timeout || 8000);
    const started = Date.now();
    while (Date.now() - started < timeout) {
      const found = await evaluate(tab.id, `
(() => {
  const el = document.querySelector(${JSON.stringify(selector)});
  if (!el) return false;
  const s = getComputedStyle(el);
  const r = el.getBoundingClientRect();
  return s.display !== 'none' && s.visibility !== 'hidden' && r.width > 1 && r.height > 1;
})()`).catch(() => false);
      if (found) return { found: true, selector, elapsedMs: Date.now() - started, tabId: tab.id };
      await new Promise(r => setTimeout(r, 250));
    }
    return { found: false, selector, elapsedMs: Date.now() - started, tabId: tab.id };
  }

  async function getText(args = {}) {
    const tab = await getTargetTab(args.tabId);
    const selector = args.selector || '';
    const maxLength = Number(args.maxLength || args.max_length || 8000);
    const value = await evaluate(tab.id, `
(() => {
  const el = ${selector ? `document.querySelector(${JSON.stringify(selector)})` : 'document.body'};
  if (!el) return { text: '', error: '元素未找到' };
  return {
    title: document.title,
    url: location.href,
    text: (el.innerText || el.textContent || '').slice(0, ${Math.max(100, maxLength)})
  };
})()`);
    return { tabId: tab.id, ...value };
  }

  async function screenshot(args = {}) {
    const tab = await getTargetTab(args.tabId);
    await ensureAttached(tab.id);
    let params = { format: args.format || 'png', fromSurface: true };
    if (params.format === 'jpeg') params.quality = Number(args.quality || 85);
    if (args.fullPage) {
      const metrics = await sendCommand(tab.id, 'Page.getLayoutMetrics');
      const size = metrics.contentSize;
      params.captureBeyondViewport = true;
      params.clip = { x: 0, y: 0, width: size.width, height: size.height, scale: 1 };
    }
    const result = await sendCommand(tab.id, 'Page.captureScreenshot', params);
    return {
      tabId: tab.id,
      mime: params.format === 'jpeg' ? 'image/jpeg' : 'image/png',
      dataUrl: `data:${params.format === 'jpeg' ? 'image/jpeg' : 'image/png'};base64,${result.data}`,
      bytesBase64: result.data?.length || 0
    };
  }

  async function pdf(args = {}) {
    const tab = await getTargetTab(args.tabId);
    await ensureAttached(tab.id);
    const result = await sendCommand(tab.id, 'Page.printToPDF', {
      printBackground: args.printBackground !== false,
      landscape: !!args.landscape
    });
    return { tabId: tab.id, mime: 'application/pdf', dataBase64: result.data, bytesBase64: result.data?.length || 0 };
  }

  async function listTabs() {
    const tabs = await chromeCall(chrome.tabs.query, { currentWindow: true });
    return {
      tabs: tabs.map(t => ({
        id: t.id,
        title: (t.title || '').slice(0, 120),
        url: (t.url || '').slice(0, 300),
        active: !!t.active,
        automation: t.id === globalThis._automationTabId,
        cdpAttached: attachedTabs.has(t.id)
      })),
      automationTabId: globalThis._automationTabId || null
    };
  }

  async function close(args = {}) {
    const tab = await getTargetTab(args.tabId);
    await detach(tab.id).catch(() => {});
    await chromeCall(chrome.tabs.remove, tab.id);
    if (globalThis._automationTabId === tab.id) globalThis._automationTabId = null;
    return { closed: true, tabId: tab.id };
  }

  function mapMessage(message = {}) {
    const args = { ...message };
    delete args.type;
    switch (message.type) {
      case 'BROWSER_OPEN': return open(args);
      case 'BROWSER_NAVIGATE': return navigate(args);
      case 'BROWSER_CLICK': return click(args);
      case 'BROWSER_FILL':
      case 'BROWSER_TYPE': return type(args);
      case 'BROWSER_PRESS': return press(args);
      case 'BROWSER_WAIT': return waitFor(args);
      case 'BROWSER_GET_TEXT': return getText(args);
      case 'BROWSER_SCREENSHOT': return screenshot(args);
      case 'BROWSER_PDF': return pdf(args);
      case 'BROWSER_LIST_TABS': return listTabs(args);
      case 'BROWSER_CLOSE': return close(args);
      case 'BROWSER_DETACH': return detach(args.tabId || globalThis._automationTabId);
      default: throw new Error('未知浏览器工具: ' + message.type);
    }
  }

  chrome.tabs?.onRemoved?.addListener((tabId) => {
    attachedTabs.delete(tabId);
    if (globalThis._automationTabId === tabId) globalThis._automationTabId = null;
  });

  globalThis.AiscCdp = {
    open,
    navigate,
    click,
    type,
    press,
    waitFor,
    getText,
    screenshot,
    pdf,
    listTabs,
    close,
    detach,
    handleMessage: mapMessage
  };
})();
