const STORAGE_KEY = "gmaps_leads_v3";
const PENDING_JOB_KEY = "gmaps_pending_job_v1";
const ACTIVE_COLLECTION_KEY = "gmaps_active_collection_v1";
const LICENSE_KEY = "gmaps_license_v1";
const DEVICE_ID_KEY = "gmaps_device_id_v1";
const DEVICE_FINGERPRINT_VERSION = "v2";
const WEBSITE_TIMEOUT_MS = 12000;
const WEBSITE_MAX_PAGES = 6;
const WEBSITE_FETCH_CONCURRENCY = 3;
const WEBSITE_ENRICH_CACHE_TTL_MS = 30 * 60 * 1000;
const WEBSITE_ENRICH_CACHE_MAX_ENTRIES = 200;
const LAUNCHER_PAGE = "popup.html";
const LAUNCHER_WINDOW_WIDTH = 560;
const LAUNCHER_WINDOW_HEIGHT = 760;
const LICENSE_CONFIG = {
  productName: "智枢获客系统",
  apiBaseUrl: "https://www.agentcoreos.com/license-api",
  activatePath: "/license/activate",
  validatePath: "/license/validate"
};
const LICENSE_VALIDATE_INTERVAL_MS = 6 * 60 * 60 * 1000;
const LICENSE_VALIDATE_SOON_INTERVAL_MS = 60 * 60 * 1000;
const LICENSE_SOON_THRESHOLD_MS = 24 * 60 * 60 * 1000;
const LICENSE_REQUEST_TIMEOUT_MS = 15000;
const EMAIL_REGEX = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
const SOCIAL_PATTERNS = {
  facebook: /https?:\/\/(?:www\.)?(?:facebook|fb)\.com\/[A-Za-z0-9._\-/?=&%]+/gi,
  instagram: /https?:\/\/(?:www\.)?(?:instagram\.com|instagr\.am)\/[A-Za-z0-9._\-/?=&%]+/gi,
  linkedin: /https?:\/\/(?:[\w-]+\.)?linkedin\.com\/[A-Za-z0-9._\-/?=&%]+/gi,
  youtube: /https?:\/\/(?:www\.)?youtube\.com\/[A-Za-z0-9._\-/?=&%]+/gi,
  twitter: /https?:\/\/(?:www\.)?(?:twitter\.com|x\.com)\/[A-Za-z0-9._\-/?=&%]+/gi,
  whatsapp: /https?:\/\/(?:wa\.me|api\.whatsapp\.com|chat\.whatsapp\.com|www\.whatsapp\.com)\/[A-Za-z0-9._\-/?=&%]+/gi
};
const DISCOVERY_PAGE_KEYWORDS = [
  "contact",
  "contact-us",
  "contactus",
  "about",
  "about-us",
  "aboutus",
  "team",
  "our-team",
  "support",
  "help",
  "customer-service",
  "service",
  "company",
  "sales",
  "faq",
  "location"
];
const DISCOVERY_DEFAULT_PATHS = [
  "/contact",
  "/contact-us",
  "/about",
  "/about-us",
  "/team",
  "/support",
  "/help",
  "/sales",
  "/faq"
];
const SOCIAL_BLOCKLIST = /(\/share|\/sharer|\/privacy|\/terms|\/policies|\/policy|\/legal|\/login|\/signup|\/home|\/intent\/|\/intent\?|\/search\?|\/hashtag\/|\/messages|\/watch\?|\/reel\/|\/reels\/|\/p\/|\/pg\/|\/settings|\/download|\/tr\?|\/dialog\/)/i;
const DOMAIN_SUFFIX_TOKENS = new Set([
  "www",
  "com",
  "net",
  "org",
  "co",
  "io",
  "me",
  "app",
  "biz",
  "info",
  "store",
  "shop",
  "site",
  "online",
  "us",
  "uk",
  "cn",
  "cc",
  "tv",
  "nyc"
]);
const BUSINESS_NAME_STOP_WORDS = new Set([
  "the",
  "and",
  "of",
  "for",
  "inc",
  "llc",
  "ltd",
  "corp",
  "corporation",
  "company",
  "co",
  "ny",
  "nyc"
]);
const BUSINESS_NAME_WEAK_TOKENS = new Set([
  "door",
  "doors",
  "window",
  "windows",
  "service",
  "services",
  "supplier",
  "suppliers",
  "contractor",
  "contractors",
  "store",
  "stores",
  "shop",
  "shops",
  "brooklyn",
  "manhattan",
  "queens",
  "bronx",
  "staten",
  "island",
  "new",
  "york",
  "city"
]);
const BUSINESS_NAME_ACRONYM_EXCLUDED_TOKENS = new Set([
  "the",
  "and",
  "of",
  "for",
  "inc",
  "llc",
  "ltd",
  "corp",
  "corporation",
  "co",
  "sdn",
  "bhd",
  "plc",
  "pte",
  "pty",
  "group",
  "holding",
  "holdings"
]);
const GENERIC_EMAIL_PROVIDER_ROOTS = new Set([
  "gmail.com",
  "googlemail.com",
  "outlook.com",
  "hotmail.com",
  "live.com",
  "msn.com",
  "yahoo.com",
  "ymail.com",
  "icloud.com",
  "me.com",
  "aol.com",
  "proton.me",
  "protonmail.com",
  "gmx.com"
]);
const SUSPICIOUS_EMAIL_PROVIDER_ROOTS = new Set([
  "wixpress.com",
  "sentry.io",
  "bugsnag.com",
  "rollbar.com",
  "newrelic.com",
  "datadoghq.com"
]);
const websiteEnrichmentCache = new Map();
const websiteEnrichmentPending = new Map();

function createDefaultCollectionState(overrides = {}) {
  return {
    count: 0,
    running: false,
    runStats: {
      success: 0,
      deduped: 0,
      skipped: 0,
      failed: 0,
      fallbackSaved: 0
    },
    diagnostics: {
      candidates: 0,
      queued: 0,
      network: 0,
      end: false
    },
    status: {
      text: "等待开始采集。",
      tone: "neutral"
    },
    updatedAt: Date.now(),
    ...overrides
  };
}

function sanitizeCollectionState(value) {
  const source = value && typeof value === "object" ? value : {};
  const defaultState = createDefaultCollectionState();
  return {
    count: Number(source.count ?? defaultState.count) || 0,
    running: Boolean(source.running ?? defaultState.running),
    runStats: {
      success: Number(source.runStats?.success ?? defaultState.runStats.success) || 0,
      deduped: Number(source.runStats?.deduped ?? defaultState.runStats.deduped) || 0,
      skipped: Number(source.runStats?.skipped ?? defaultState.runStats.skipped) || 0,
      failed: Number(source.runStats?.failed ?? defaultState.runStats.failed) || 0,
      fallbackSaved: Number(source.runStats?.fallbackSaved ?? defaultState.runStats.fallbackSaved) || 0
    },
    diagnostics: {
      candidates: Number(source.diagnostics?.candidates ?? defaultState.diagnostics.candidates) || 0,
      queued: Number(source.diagnostics?.queued ?? defaultState.diagnostics.queued) || 0,
      network: Number(source.diagnostics?.network ?? defaultState.diagnostics.network) || 0,
      end: Boolean(source.diagnostics?.end ?? defaultState.diagnostics.end)
    },
    status: {
      text: normalizeText(source.status?.text || defaultState.status.text) || defaultState.status.text,
      tone: normalizeText(source.status?.tone || defaultState.status.tone) || defaultState.status.tone
    },
    updatedAt: Number(source.updatedAt) || Date.now()
  };
}

async function updateActiveCollectionState(collectionId, nextState) {
  const activeCollection = await getActiveCollection();
  if (!activeCollection?.collectionId || normalizeText(collectionId) !== activeCollection.collectionId) {
    return null;
  }

  const mergedState = sanitizeCollectionState({
    ...(activeCollection.state || {}),
    ...(nextState || {}),
    runStats: {
      ...(activeCollection.state?.runStats || {}),
      ...(nextState?.runStats || {})
    },
    diagnostics: {
      ...(activeCollection.state?.diagnostics || {}),
      ...(nextState?.diagnostics || {})
    },
    status: {
      ...(activeCollection.state?.status || {}),
      ...(nextState?.status || {})
    },
    updatedAt: Date.now()
  });

  const nextCollection = {
    ...activeCollection,
    state: mergedState
  };
  await setActiveCollection(nextCollection);
  await consumePendingJobForCollection(collectionId, mergedState);
  return nextCollection;
}

function getStorage() {
  return new Promise((resolve) => {
    chrome.storage.local.get({ [STORAGE_KEY]: [] }, (result) => {
      const stored = Array.isArray(result[STORAGE_KEY]) ? result[STORAGE_KEY] : [];
      const normalized = sanitizeSharedWebsites(stored.map((lead) => normalizeLead(lead))).map((lead) => normalizeLead(lead));
      if (JSON.stringify(stored) !== JSON.stringify(normalized)) {
        chrome.storage.local.set({ [STORAGE_KEY]: normalized }, () => resolve(normalized));
        return;
      }

      resolve(normalized);
    });
  });
}

function setStorage(leads) {
  return new Promise((resolve) => {
    chrome.storage.local.set({ [STORAGE_KEY]: leads }, resolve);
  });
}

function getPendingJob() {
  return new Promise((resolve) => {
    chrome.storage.local.get({ [PENDING_JOB_KEY]: null }, (result) => {
      resolve(result[PENDING_JOB_KEY] || null);
    });
  });
}

function setPendingJob(job) {
  return new Promise((resolve) => {
    chrome.storage.local.set({ [PENDING_JOB_KEY]: job }, resolve);
  });
}

function clearPendingJob() {
  return new Promise((resolve) => {
    chrome.storage.local.remove(PENDING_JOB_KEY, resolve);
  });
}

function normalizeSearchableMapsUrl(value) {
  const text = normalizeText(value).replace(/\+/g, " ");
  if (!text) {
    return "";
  }

  try {
    return decodeURIComponent(text).toLowerCase();
  } catch (_error) {
    return text.toLowerCase();
  }
}

function doesMapsUrlMatchCollectionQuery(url, query) {
  const normalizedUrl = normalizeSearchableMapsUrl(url);
  const normalizedQuery = normalizeText(query).toLowerCase();
  if (!normalizedUrl || !normalizedQuery) {
    return false;
  }

  return normalizedUrl.includes(normalizedQuery);
}

async function consumePendingJobForCollection(collectionId, collectionState) {
  if (!collectionState?.running) {
    return;
  }

  const pendingJob = await getPendingJob();
  if (!pendingJob?.collectionId) {
    return;
  }

  if (normalizeText(pendingJob.collectionId) !== normalizeText(collectionId)) {
    return;
  }

  await clearPendingJob();
}

function getActiveCollection() {
  return new Promise((resolve) => {
    chrome.storage.local.get({ [ACTIVE_COLLECTION_KEY]: null }, (result) => {
      resolve(result[ACTIVE_COLLECTION_KEY] || null);
    });
  });
}

function setActiveCollection(collection) {
  return new Promise((resolve) => {
    chrome.storage.local.set({ [ACTIVE_COLLECTION_KEY]: collection }, resolve);
  });
}

function getLocalValue(key, fallbackValue) {
  return new Promise((resolve) => {
    chrome.storage.local.get({ [key]: fallbackValue }, (result) => {
      resolve(result[key] ?? fallbackValue);
    });
  });
}

function setLocalValues(values) {
  return new Promise((resolve) => {
    chrome.storage.local.set(values, resolve);
  });
}

function removeLocalValue(key) {
  return new Promise((resolve) => {
    chrome.storage.local.remove(key, resolve);
  });
}

function createDeviceId() {
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
    return `dev_${crypto.randomUUID().replace(/-/g, "").slice(0, 16)}`;
  }

  const bytes = new Uint8Array(8);
  crypto.getRandomValues(bytes);
  return `dev_${[...bytes].map((value) => value.toString(16).padStart(2, "0")).join("")}`;
}

function createCollectionId() {
  if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
    return `job_${crypto.randomUUID().replace(/-/g, "").slice(0, 20)}`;
  }

  const bytes = new Uint8Array(10);
  crypto.getRandomValues(bytes);
  return `job_${[...bytes].map((value) => value.toString(16).padStart(2, "0")).join("")}`;
}

async function getDeviceId() {
  const existing = await getLocalValue(DEVICE_ID_KEY, "");
  if (existing) {
    return existing;
  }

  const deviceId = createDeviceId();
  await setLocalValues({ [DEVICE_ID_KEY]: deviceId });
  return deviceId;
}

function getPlatformInfo() {
  return new Promise((resolve) => {
    chrome.runtime.getPlatformInfo((platformInfo) => {
      resolve(platformInfo || {});
    });
  });
}

async function hashText(value) {
  const encoder = new TextEncoder();
  const bytes = encoder.encode(String(value || ""));
  const digest = await crypto.subtle.digest("SHA-256", bytes);
  return [...new Uint8Array(digest)].map((item) => item.toString(16).padStart(2, "0")).join("");
}

async function getDeviceFingerprintKey() {
  const platformInfo = await getPlatformInfo();
  const fingerprintSource = JSON.stringify({
    version: DEVICE_FINGERPRINT_VERSION,
    runtimeId: chrome.runtime.id,
    userAgent: normalizeText(navigator.userAgent),
    language: normalizeText(navigator.language),
    languages: Array.isArray(navigator.languages) ? navigator.languages.join(",") : "",
    platform: normalizeText(navigator.platform),
    hardwareConcurrency: Number(navigator.hardwareConcurrency || 0),
    deviceMemory: Number(navigator.deviceMemory || 0),
    maxTouchPoints: Number(navigator.maxTouchPoints || 0),
    timezone: normalizeText(Intl.DateTimeFormat().resolvedOptions().timeZone),
    os: normalizeText(platformInfo.os),
    arch: normalizeText(platformInfo.arch),
    naclArch: normalizeText(platformInfo.nacl_arch)
  });

  const fingerprintHash = await hashText(fingerprintSource);
  return `fp_${fingerprintHash.slice(0, 32)}`;
}

async function getDeviceIdentity() {
  const [deviceId, fingerprintKey] = await Promise.all([
    getDeviceId(),
    getDeviceFingerprintKey()
  ]);

  return {
    deviceId,
    fingerprintKey
  };
}

function normalizeActivationCode(value) {
  return normalizeText(value).toUpperCase();
}

function sanitizeUserFacingError(error) {
  const message = normalizeText(error && error.message);

  if (!message) {
    return "操作失败，请稍后重试。";
  }

  if (
    /请输入激活码|授权有效|授权已到期|授权已停用|激活码不存在|激活码无效|达到设备绑定上限|按首次激活时间计算|请求过于频繁|设备请求异常|插件未激活|授权服务地址未配置|当前设备未绑定/.test(message)
  ) {
    return message;
  }

  if (/Failed to fetch|NetworkError|HTTP 5\d{2}|请求失败|timeout|network/i.test(message)) {
    return "授权服务暂时不可用，请稍后重试。";
  }

  return message;
}

function isLicenseApiConfigured() {
  return Boolean(normalizeText(LICENSE_CONFIG.apiBaseUrl));
}

function normalizeLicenseStatus(status, expiresAt) {
  const normalizedStatus = normalizeText(status).toLowerCase();
  const expiresAtMs = expiresAt ? Date.parse(expiresAt) : NaN;

  if (Number.isFinite(expiresAtMs) && Date.now() >= expiresAtMs) {
    return "expired";
  }

  if (["active", "expired", "disabled", "invalid"].includes(normalizedStatus)) {
    return normalizedStatus;
  }

  return "unlicensed";
}

function formatPlanType(planType) {
  switch (normalizeText(planType).toLowerCase()) {
    case "weekly":
      return "体验周卡";
    case "yearly":
      return "年卡";
    default:
      return normalizeText(planType);
  }
}

function getLicenseMessage(status, isConfigured) {
  switch (status) {
    case "active":
      return "授权有效。";
    case "expired":
      return "授权已到期，请更换新的激活码。";
    case "disabled":
      return "授权已停用，请联系管理员。";
    case "invalid":
      return "激活码无效，请检查后重试。";
    default:
      return isConfigured ? "插件未激活，请先输入激活码。" : "插件未激活，请先输入激活码。";
  }
}

function buildLicenseState(record, deviceId) {
  const baseRecord = record && typeof record === "object" ? record : {};
  const code = normalizeActivationCode(baseRecord.code);
  const planType = normalizeText(baseRecord.planType).toLowerCase();
  const activatedAt = normalizeText(baseRecord.activatedAt);
  const expiresAt = normalizeText(baseRecord.expiresAt);
  const lastValidatedAt = normalizeText(baseRecord.lastValidatedAt);
  const nextCheckAt = normalizeText(baseRecord.nextCheckAt);
  const status = normalizeLicenseStatus(baseRecord.status, expiresAt);
  const isConfigured = isLicenseApiConfigured();

  return {
    code,
    planType,
    planLabel: formatPlanType(planType),
    status,
    isActive: status === "active",
    message: getLicenseMessage(status, isConfigured),
    activatedAt,
    expiresAt,
    lastValidatedAt,
    nextCheckAt,
    deviceId,
    isConfigured,
    productName: LICENSE_CONFIG.productName,
    reusedExistingLicense: Boolean(baseRecord.reusedExistingLicense)
  };
}

async function getStoredLicense() {
  return getLocalValue(LICENSE_KEY, null);
}

async function setStoredLicense(license) {
  await setLocalValues({ [LICENSE_KEY]: license });
}

async function clearStoredLicense() {
  await removeLocalValue(LICENSE_KEY);
}

function createStoredLicenseFromApi(payload, deviceId, fallbackCode) {
  const nowMs = Date.now();
  const now = new Date(nowMs).toISOString();

  return {
    code: normalizeActivationCode(payload.code || fallbackCode),
    planType: normalizeText(payload.planType).toLowerCase(),
    status: normalizeText(payload.status).toLowerCase() || "active",
    activatedAt: normalizeText(payload.activatedAt),
    expiresAt: normalizeText(payload.expiresAt),
    lastValidatedAt: now,
    nextCheckAt: getNextLicenseCheckAt(payload.expiresAt, nowMs),
    deviceId,
    reusedExistingLicense: Boolean(payload.reusedExistingLicense)
  };
}

async function requestLicenseApi(path, payload) {
  const apiBaseUrl = normalizeText(LICENSE_CONFIG.apiBaseUrl).replace(/\/+$/, "");
  if (!apiBaseUrl) {
    throw new Error("授权服务地址未配置。");
  }

  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), LICENSE_REQUEST_TIMEOUT_MS);
  let response;

  try {
    response = await fetch(`${apiBaseUrl}${path}`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(payload),
      signal: controller.signal
    });
  } catch (error) {
    if (controller.signal.aborted) {
      throw new Error("授权服务请求超时，请稍后重试。");
    }

    throw new Error(error?.message || "授权服务请求失败。");
  } finally {
    clearTimeout(timeoutId);
  }

  let data = {};

  try {
    data = await response.json();
  } catch (_error) {
    data = {};
  }

  if (!response.ok || data.ok === false) {
    throw new Error(data.error || `授权服务请求失败：HTTP ${response.status}`);
  }

  return data;
}

function getNextLicenseCheckAt(expiresAt, nowMs = Date.now()) {
  const expiresAtMs = Date.parse(normalizeText(expiresAt));
  const intervalMs =
    Number.isFinite(expiresAtMs) && expiresAtMs - nowMs <= LICENSE_SOON_THRESHOLD_MS
      ? LICENSE_VALIDATE_SOON_INTERVAL_MS
      : LICENSE_VALIDATE_INTERVAL_MS;
  const nextCheckMs = nowMs + intervalMs;

  if (Number.isFinite(expiresAtMs)) {
    return new Date(Math.min(expiresAtMs, nextCheckMs)).toISOString();
  }

  return new Date(nextCheckMs).toISOString();
}

function getLicenseNextCheckAtMs(licenseState) {
  const explicitCheckAtMs = Date.parse(normalizeText(licenseState?.nextCheckAt));
  if (Number.isFinite(explicitCheckAtMs)) {
    return explicitCheckAtMs;
  }

  const lastValidatedAtMs = Date.parse(normalizeText(licenseState?.lastValidatedAt));
  if (!Number.isFinite(lastValidatedAtMs)) {
    return NaN;
  }

  return Date.parse(getNextLicenseCheckAt(licenseState?.expiresAt, lastValidatedAtMs));
}

function shouldUseCachedLicenseState(licenseState, options = {}) {
  if (options.forceValidate || !licenseState?.code) {
    return false;
  }

  const nextCheckAtMs = getLicenseNextCheckAtMs(licenseState);
  return Number.isFinite(nextCheckAtMs) && nextCheckAtMs > Date.now();
}

async function getLicenseState(options = {}) {
  const { deviceId, fingerprintKey } = await getDeviceIdentity();
  const storedLicense = await getStoredLicense();
  const baseState = buildLicenseState(storedLicense, deviceId);

  if (!baseState.code) {
    return baseState;
  }

  const normalizedStatus = normalizeLicenseStatus(baseState.status, baseState.expiresAt);
  if (!isLicenseApiConfigured()) {
    return {
      ...baseState,
      status: normalizedStatus,
      isActive: normalizedStatus === "active",
      message: "授权服务地址未配置。"
    };
  }

  if (shouldUseCachedLicenseState(baseState, options)) {
    return {
      ...baseState,
      status: normalizedStatus,
      isActive: normalizedStatus === "active"
    };
  }

  try {
    const payload = await requestLicenseApi(LICENSE_CONFIG.validatePath, {
      code: baseState.code,
      deviceId,
      fingerprintKey,
      clientVersion: chrome.runtime.getManifest().version
    });
    const nextLicense = createStoredLicenseFromApi(payload, deviceId, baseState.code);
    await setStoredLicense(nextLicense);
    return buildLicenseState(nextLicense, deviceId);
  } catch (error) {
    if (storedLicense) {
      const fallbackState = {
        ...baseState,
        status: normalizedStatus,
        isActive: normalizedStatus === "active",
        message: error.message || getLicenseMessage(normalizedStatus, true)
      };

      if (options.forceValidate) {
        fallbackState.validationError = fallbackState.message;
      }

      return fallbackState;
    }

    return {
      ...baseState,
      message: error.message || "读取授权状态失败。"
    };
  }
}

async function activateLicense(code) {
  const normalizedCode = normalizeActivationCode(code);
  if (!normalizedCode) {
    throw new Error("请输入激活码。");
  }

  const { deviceId, fingerprintKey } = await getDeviceIdentity();
  const payload = await requestLicenseApi(LICENSE_CONFIG.activatePath, {
    code: normalizedCode,
    deviceId,
    fingerprintKey,
    clientVersion: chrome.runtime.getManifest().version
  });
  const storedLicense = createStoredLicenseFromApi(payload, deviceId, normalizedCode);
  await setStoredLicense(storedLicense);
  return buildLicenseState(storedLicense, deviceId);
}

function createLicenseError(licenseState) {
  const error = new Error(licenseState.message);
  error.license = licenseState;
  return error;
}

async function ensureLicenseActive() {
  const licenseState = await getLicenseState();
  if (!licenseState.isActive) {
    throw createLicenseError(licenseState);
  }

  return licenseState;
}

function createTab(url) {
  return new Promise((resolve, reject) => {
    chrome.tabs.create({ url }, (tab) => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
        return;
      }

      resolve(tab);
    });
  });
}

function fetchExtensionPage(url) {
  return new Promise((resolve, reject) => {
    fetch(url, { cache: "no-store" })
      .then((response) => resolve(response))
      .catch((error) => reject(error));
  });
}

async function ensureExtensionPageAvailable(page) {
  const url = chrome.runtime.getURL(page);
  const response = await fetchExtensionPage(url);
  if (!response.ok) {
    throw new Error(`扩展页面不可用：${page}（HTTP ${response.status}）。请确认 Chrome 当前加载的是包含该文件的扩展目录。`);
  }

  return url;
}

function queryTabs(queryInfo) {
  return new Promise((resolve, reject) => {
    chrome.tabs.query(queryInfo, (tabs) => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
        return;
      }

      resolve(tabs || []);
    });
  });
}

function updateTab(tabId, updateProperties) {
  return new Promise((resolve, reject) => {
    chrome.tabs.update(tabId, updateProperties, (tab) => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
        return;
      }

      resolve(tab);
    });
  });
}

function updateWindow(windowId, updateInfo) {
  return new Promise((resolve, reject) => {
    chrome.windows.update(windowId, updateInfo, (window) => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
        return;
      }

      resolve(window);
    });
  });
}

function createWindow(createData) {
  return new Promise((resolve, reject) => {
    chrome.windows.create(createData, (window) => {
      if (chrome.runtime.lastError) {
        reject(new Error(chrome.runtime.lastError.message));
        return;
      }

      resolve(window);
    });
  });
}

async function openOrFocusLauncherWindow() {
  const launcherUrl = chrome.runtime.getURL(LAUNCHER_PAGE);
  const existingTabs = await queryTabs({ url: launcherUrl });
  const existingTab = existingTabs.find((tab) => Number.isInteger(tab.windowId));

  if (existingTab) {
    await updateWindow(existingTab.windowId, { focused: true });
    await updateTab(existingTab.id, { active: true });
    return existingTab;
  }

  const launcherWindow = await createWindow({
    url: launcherUrl,
    type: "popup",
    focused: true,
    width: LAUNCHER_WINDOW_WIDTH,
    height: LAUNCHER_WINDOW_HEIGHT
  });

  return launcherWindow;
}

function normalizeText(value) {
  return String(value || "").replace(/\s+/g, " ").trim();
}

async function initializeCollectionSession(query, enrichEmail = true) {
  const normalizedQuery = normalizeText(query);
  if (!normalizedQuery) {
    throw new Error("请输入搜索关键词。");
  }

  const session = {
    collectionId: createCollectionId(),
    query: normalizedQuery,
    createdAt: Date.now(),
    enrichEmail: enrichEmail !== false,
    state: createDefaultCollectionState({
      status: {
        text: "等待开始采集。",
        tone: "neutral"
      }
    })
  };

  await setStorage([]);
  await setActiveCollection(session);
  return session;
}

function normalizeUrl(value) {
  const text = normalizeText(value);
  if (!text) {
    return "";
  }

  try {
    const url = new URL(text);
    url.hash = "";
    return url.toString();
  } catch (_error) {
    return text;
  }
}

function isSocialUrl(value) {
  const text = normalizeText(value);
  return /facebook\.com|fb\.com|instagram\.com|instagr\.am|linkedin\.com|youtube\.com|youtu\.be|twitter\.com|x\.com|whatsapp\.com|wa\.me/i.test(text);
}

function getHostnameTokens(value) {
  const url = normalizeUrl(value);
  if (!url) {
    return [];
  }

  try {
    return new URL(url).hostname
      .toLowerCase()
      .split(/[^a-z0-9]+/i)
      .map((token) => token.trim())
      .filter((token) => token && !DOMAIN_SUFFIX_TOKENS.has(token));
  } catch (_error) {
    return [];
  }
}

function getDistinctiveBusinessNameTokens(value) {
  return normalizeText(value)
    .toLowerCase()
    .replace(/&/g, " and ")
    .split(/[^a-z0-9]+/i)
    .map((token) => token.trim())
    .filter((token) => token && token.length >= 2)
    .filter((token) => !BUSINESS_NAME_STOP_WORDS.has(token))
    .filter((token) => !BUSINESS_NAME_WEAK_TOKENS.has(token));
}

function getBusinessNameAcronyms(value) {
  const rawTokens = normalizeText(value)
    .toLowerCase()
    .replace(/&/g, " and ")
    .split(/[^a-z0-9]+/i)
    .map((token) => token.trim())
    .filter(Boolean)
    .filter((token) => !BUSINESS_NAME_ACRONYM_EXCLUDED_TOKENS.has(token));

  const acronyms = new Set();
  for (let start = 0; start < rawTokens.length; start += 1) {
    for (let size = 2; size <= 4 && start + size <= rawTokens.length; size += 1) {
      const acronym = rawTokens.slice(start, start + size).map((token) => token[0]).join("");
      if (acronym.length >= 2 && acronym.length <= 6) {
        acronyms.add(acronym);
      }
    }
  }

  return [...acronyms];
}

function urlMatchesBusinessName(value, businessName) {
  const tokens = getDistinctiveBusinessNameTokens(businessName);
  const acronyms = getBusinessNameAcronyms(businessName);
  if (!tokens.length && !acronyms.length) {
    return false;
  }

  const hostnameTokens = getHostnameTokens(value);
  if (!hostnameTokens.length) {
    return false;
  }

  const joinedHostname = hostnameTokens.join("");
  const tokenMatched = tokens.some((token) => token.length >= 4 && (
    hostnameTokens.some((hostnameToken) => hostnameToken.includes(token) || token.includes(hostnameToken)) ||
    joinedHostname.includes(token)
  ));
  if (tokenMatched) {
    return true;
  }

  return acronyms.some((acronym) => {
    if (acronym.length < 2) {
      return false;
    }

    return hostnameTokens.some((hostnameToken) => hostnameToken === acronym || hostnameToken.includes(acronym)) ||
      joinedHostname.includes(acronym);
  });
}

function isBlockedWebsiteUrl(value) {
  const url = normalizeUrl(value);
  if (!url) {
    return true;
  }

  try {
    const { hostname } = new URL(url);
    return /(^|\.)google\./i.test(hostname) ||
      /(^|\.)gstatic\.com$/i.test(hostname) ||
      /(^|\.)googleusercontent\.com$/i.test(hostname) ||
      /(^|\.)tripadvisor\./i.test(hostname) ||
      /(^|\.)ubereats\.com$/i.test(hostname) ||
      /(^|\.)doordash\.com$/i.test(hostname) ||
      /(^|\.)grubhub\.com$/i.test(hostname) ||
      /(^|\.)postmates\.com$/i.test(hostname) ||
      /(^|\.)wolt\.com$/i.test(hostname) ||
      /(^|\.)bolt\.eu$/i.test(hostname) ||
      /(^|\.)foody\.com\.cy$/i.test(hostname) ||
      /(^|\.)deliveroo\./i.test(hostname) ||
      /(^|\.)opentable\./i.test(hostname) ||
      /(^|\.)resy\.com$/i.test(hostname) ||
      /(^|\.)tablecheck\.com$/i.test(hostname) ||
      /(^|\.)linktr\.ee$/i.test(hostname) ||
      /(^|\.)bit\.ly$/i.test(hostname);
  } catch (_error) {
    return true;
  }
}

function isGenericLocationLandingUrl(value) {
  const url = normalizeUrl(value);
  if (!url) {
    return false;
  }

  try {
    const { pathname } = new URL(url);
    return /^\/(?:location|locations|service-area|service-areas|area|areas|city|cities)\/[a-z0-9-]+\/?$/i.test(pathname);
  } catch (_error) {
    return false;
  }
}

function isLikelyOfficialWebsiteUrl(value, businessName = "", options = {}) {
  const url = normalizeUrl(value);
  if (!url || isSocialUrl(url) || isBlockedWebsiteUrl(url) || /^mailto:|^tel:/i.test(url)) {
    return false;
  }

  if (isGenericLocationLandingUrl(url) && !urlMatchesBusinessName(url, businessName)) {
    return false;
  }

  if (options.requireNameMatch) {
    return urlMatchesBusinessName(url, businessName);
  }

  return true;
}

function getUrlRootDomain(value) {
  const url = normalizeUrl(value);
  if (!url) {
    return "";
  }

  try {
    return getRootDomain(new URL(url).hostname);
  } catch (_error) {
    return "";
  }
}

function getEmailRootDomain(value) {
  const email = isValidEmailCandidate(value);
  if (!email) {
    return "";
  }

  const domain = email.split("@")[1] || "";
  return getRootDomain(domain);
}

function clearSuspiciousContactFields(lead, context = {}) {
  const nextLead = { ...lead };
  const businessName = normalizeText(nextLead.name);
  const website = normalizeUrl(nextLead.website);
  const websiteCleared = Boolean(context.clearWebsite);

  if (websiteCleared) {
    nextLead.website = "";
  }

  const emailDomain = getEmailRootDomain(nextLead.email);
  const matchedEmail = emailDomain && urlMatchesBusinessName(`https://${emailDomain}`, businessName);
  const matchedWebsite = website && urlMatchesBusinessName(website, businessName);
  const websiteRootDomain = getUrlRootDomain(website);
  const emailMatchesWebsite = emailDomain && websiteRootDomain && emailDomain === websiteRootDomain;

  if (
    nextLead.email &&
    (
      (websiteCleared && emailMatchesWebsite) ||
      (
        emailDomain &&
        !matchedEmail &&
        website &&
        !matchedWebsite &&
        !emailMatchesWebsite &&
        !isGenericEmailProviderDomain(emailDomain)
      )
    )
  ) {
    nextLead.email = "";
  }

  return nextLead;
}

function sanitizeSharedWebsites(leads) {
  const normalizedLeads = Array.isArray(leads) ? leads.map((lead) => ({ ...lead })) : [];
  const groups = new Map();

  normalizedLeads.forEach((lead, index) => {
    const website = normalizeUrl(lead.website);
    const rootDomain = getUrlRootDomain(website);
    if (!website || !rootDomain) {
      return;
    }

    if (!groups.has(rootDomain)) {
      groups.set(rootDomain, []);
    }

    groups.get(rootDomain).push({ lead, index, website });
  });

  groups.forEach((entries) => {
    if (entries.length < 2) {
      return;
    }

    const distinctNames = new Set(entries.map(({ lead }) => normalizeText(lead.name).toLowerCase()).filter(Boolean));
    if (distinctNames.size < 2) {
      return;
    }

    const matchedEntries = entries.filter(({ lead, website }) => urlMatchesBusinessName(website, lead.name));
    if (matchedEntries.length === entries.length) {
      return;
    }

    const keepMatchedOnly =
      matchedEntries.length > 0 &&
      matchedEntries.length < entries.length &&
      matchedEntries.length <= Math.max(2, Math.floor(entries.length / 2));
    entries.forEach(({ lead, index, website }) => {
      const matchesName = urlMatchesBusinessName(website, lead.name);
      if (keepMatchedOnly && matchesName) {
        normalizedLeads[index] = { ...lead };
        return;
      }

      normalizedLeads[index] = clearSuspiciousContactFields(lead, { clearWebsite: true });
    });
  });

  return normalizedLeads;
}

function normalizeTimestamp(value) {
  const text = normalizeText(value);
  const timestampMs = Date.parse(text);
  return Number.isFinite(timestampMs) ? new Date(timestampMs).toISOString() : "";
}

function normalizeLead(lead, options = {}) {
  const { touchUpdatedAt = false } = options;
  const normalized = { ...lead };

  Object.keys(normalized).forEach((key) => {
    if (typeof normalized[key] === "string") {
      normalized[key] = normalizeText(normalized[key]);
    }
  });

  normalized.website = normalizeUrl(normalized.website);
  normalized.googleMapsUrl = normalizeUrl(normalized.googleMapsUrl);
  normalized.sourceHref = normalizeUrl(normalized.sourceHref || normalized.googleMapsUrl);
  normalized.email = normalizeText(normalized.email);
  normalized.placeId = normalizeText(normalized.placeId || normalized.placeID);
  normalized.placeID = normalized.placeId;
  normalized.cID = normalizeText(normalized.cID || normalized.cid);
  normalized.cid = normalized.cID;
  normalized.rating = normalizeText(normalized.rating || normalized.averageRating);
  normalized.averageRating = normalized.rating;
  normalized.reviewCount = normalizeText(normalized.reviewCount);
  normalized.name = normalizeText(normalized.name);
  normalized.address = normalizeText(normalized.address);
  normalized.phone = normalizeText(normalized.phone);
  normalized.category = normalizeText(normalized.category);
  normalized.latitude = normalizeText(normalized.latitude);
  normalized.longitude = normalizeText(normalized.longitude);
  normalized.query = normalizeText(normalized.query);
  normalized.source = normalizeText(normalized.source);
  if (normalized.website && !isLikelyOfficialWebsiteUrl(normalized.website, normalized.name)) {
    normalized.website = "";
  }
  normalized.updatedAt = touchUpdatedAt ? new Date().toISOString() : normalizeTimestamp(normalized.updatedAt) || new Date().toISOString();

  return normalized;
}

function hasLeadPayloadChanged(leftLead, rightLead) {
  const leftNormalized = normalizeLead(leftLead);
  const rightNormalized = normalizeLead(rightLead);
  const { updatedAt: _leftUpdatedAt, ...leftPayload } = leftNormalized;
  const { updatedAt: _rightUpdatedAt, ...rightPayload } = rightNormalized;
  return JSON.stringify(leftPayload) !== JSON.stringify(rightPayload);
}

function buildCoordinateIdentity(lead) {
  const latitude = normalizeText(lead?.latitude);
  const longitude = normalizeText(lead?.longitude);
  if (!latitude || !longitude) {
    return "";
  }

  return [
    latitude,
    longitude,
    normalizeText(lead?.name),
    normalizeText(lead?.phone)
  ].filter(Boolean).join("|");
}

function getLeadKey(lead) {
  const normalized = normalizeLead(lead);

  return (
    normalized.placeId ||
    normalized.cID ||
    buildCoordinateIdentity(normalized) ||
    normalized.sourceHref ||
    normalized.googleMapsUrl ||
    [
      normalized.name,
      normalized.address,
      normalized.phone,
      normalized.website
    ]
      .map(normalizeText)
      .filter(Boolean)
      .join("|")
  );
}

function getSourceRank(source) {
  switch (normalizeText(source).toLowerCase()) {
    case "dom":
      return 3;
    case "network":
      return 2;
    case "fallback":
      return 1;
    default:
      return 0;
  }
}

function shouldPreferIncomingValue(key, currentValue, incomingValue, existingLead, incomingLead) {
  if (!currentValue) {
    return true;
  }

  const currentSourceRank = getSourceRank(existingLead?.source);
  const incomingSourceRank = getSourceRank(incomingLead?.source);
  const normalizedCurrent = normalizeText(currentValue);
  const normalizedIncoming = normalizeText(incomingValue);

  if (!normalizedIncoming) {
    return false;
  }

  if (key === "source") {
    return incomingSourceRank > currentSourceRank;
  }

  if (key === "phone") {
    const currentDigits = normalizedCurrent.replace(/\D/g, "");
    const incomingDigits = normalizedIncoming.replace(/\D/g, "");
    return incomingDigits.length > currentDigits.length || (incomingSourceRank > currentSourceRank && incomingDigits.length >= currentDigits.length);
  }

  if (["address", "category", "openingHours", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"].includes(key)) {
    return incomingSourceRank >= currentSourceRank && normalizedIncoming.length > normalizedCurrent.length;
  }

  if (["rating", "averageRating", "reviewCount"].includes(key)) {
    return incomingSourceRank >= currentSourceRank && normalizedIncoming.length >= normalizedCurrent.length;
  }

  if (["website", "googleMapsUrl", "sourceHref", "facebook", "instagram", "linkedin", "youtube", "whatsapp", "twitter"].includes(key)) {
    const currentIsGoogle = /(^https?:\/\/)?([^/]+\.)?google\./i.test(normalizedCurrent);
    const incomingIsGoogle = /(^https?:\/\/)?([^/]+\.)?google\./i.test(normalizedIncoming);
    if (currentIsGoogle && !incomingIsGoogle) {
      return true;
    }

      if (key === "website") {
        const businessName = normalizeText(incomingLead?.name || existingLead?.name);
        const currentAccepted = isLikelyOfficialWebsiteUrl(normalizedCurrent, businessName);
        const incomingAccepted = isLikelyOfficialWebsiteUrl(normalizedIncoming, businessName);
        if (incomingAccepted !== currentAccepted) {
          return incomingAccepted;
        }

      const currentMatchesName = urlMatchesBusinessName(normalizedCurrent, businessName);
      const incomingMatchesName = urlMatchesBusinessName(normalizedIncoming, businessName);
      if (incomingMatchesName !== currentMatchesName) {
        return incomingMatchesName;
      }
    }

    return incomingSourceRank > currentSourceRank && normalizedIncoming.length >= normalizedCurrent.length;
  }

  return false;
}

function mergeLead(existingLead, incomingLead) {
  const merged = { ...existingLead };
  let didChange = false;

  Object.entries(incomingLead).forEach(([key, value]) => {
    if (key === "updatedAt") {
      return;
    }

    const incomingValue = typeof value === "string" ? normalizeText(value) : value;
    const currentValue = merged[key];

    if (Array.isArray(incomingValue)) {
      const nextValues = new Set([...(Array.isArray(currentValue) ? currentValue : []), ...incomingValue]);
      const nextArray = [...nextValues];
      if (JSON.stringify(nextArray) !== JSON.stringify(Array.isArray(currentValue) ? currentValue : [])) {
        merged[key] = nextArray;
        didChange = true;
      }
      return;
    }

    if (incomingValue === null || incomingValue === undefined || incomingValue === "") {
      return;
    }

    if (typeof currentValue === "string" && currentValue) {
      if (shouldPreferIncomingValue(key, currentValue, incomingValue, existingLead, incomingLead)) {
        if (currentValue !== incomingValue) {
          merged[key] = incomingValue;
          didChange = true;
        }
      }
      return;
    }

    if (currentValue === null || currentValue === undefined || currentValue === "") {
      merged[key] = incomingValue;
      didChange = true;
    }
  });

  merged.updatedAt = didChange ? new Date().toISOString() : normalizeTimestamp(existingLead?.updatedAt) || normalizeTimestamp(merged.updatedAt) || new Date().toISOString();
  return merged;
}

async function mergeLeads(leads) {
  const existingLeads = await getStorage();
  const leadMap = new Map();
  const stats = {
    totalIncoming: Array.isArray(leads) ? leads.length : 0,
    insertedCount: 0,
    updatedCount: 0,
    unchangedCount: 0
  };

  existingLeads.forEach((lead) => {
    const key = getLeadKey(lead);
    if (key) {
      leadMap.set(key, normalizeLead(lead));
    }
  });

  leads.forEach((lead) => {
    const normalizedLead = normalizeLead(lead, { touchUpdatedAt: true });
    const key = getLeadKey(normalizedLead);
    if (!key) {
      return;
    }

    if (leadMap.has(key)) {
      const existingLead = leadMap.get(key);
      const mergedLead = mergeLead(existingLead, normalizedLead);
      if (JSON.stringify(existingLead) === JSON.stringify(mergedLead)) {
        stats.unchangedCount += 1;
      } else {
        stats.updatedCount += 1;
      }
      leadMap.set(key, mergedLead);
      return;
    }

    stats.insertedCount += 1;
    leadMap.set(key, normalizedLead);
  });

  const mergedValues = [...leadMap.values()];
  const sanitizedValues = sanitizeSharedWebsites(mergedValues);
  const merged = sanitizedValues.map((lead, index) =>
    normalizeLead(lead, { touchUpdatedAt: hasLeadPayloadChanged(mergedValues[index], lead) })
  );
  await setStorage(merged);
  return {
    leads: merged,
    stats
  };
}

function safeUrl(input) {
  try {
    return new URL(input);
  } catch (_error) {
    return null;
  }
}

function unwrapGoogleRedirectUrl(value) {
  const text = normalizeText(value);
  if (!text) {
    return "";
  }

  try {
    const url = new URL(text);
    if (/(^|\.)google\./i.test(url.hostname) && (url.pathname === "/url" || url.pathname.includes("/redirect"))) {
      return normalizeText(url.searchParams.get("q") || url.searchParams.get("url"));
    }

    return url.toString();
  } catch (_error) {
    return text;
  }
}

function normalizeResolvableUrl(value, baseUrl = "") {
  let text = normalizeText(value).replace(/^[<(（【「『\s]+|[)>）,，。；;】」』\s]+$/g, "");
  if (!text) {
    return "";
  }

  if (text.startsWith("//")) {
    text = `https:${text}`;
  } else if (text.startsWith("/url?")) {
    text = `https://www.google.com${text}`;
  } else if (!/^[a-z][a-z\d+.-]*:/i.test(text) && /^(?:www\.)?[a-z\d][a-z\d.-]*\.[a-z]{2,}(?:[/?#]|$)/i.test(text)) {
    text = `https://${text.replace(/^\/+/, "")}`;
  }

  text = unwrapGoogleRedirectUrl(text);

  try {
    const url = baseUrl ? new URL(text, baseUrl) : new URL(text);
    if (!/^https?:$/i.test(url.protocol)) {
      return "";
    }

    url.hash = "";
    return url.toString();
  } catch (_error) {
    return "";
  }
}

function getRootDomain(hostname) {
  const normalized = normalizeText(hostname).toLowerCase().replace(/^www\./, "");
  if (!normalized) {
    return "";
  }

  const parts = normalized.split(".").filter(Boolean);
  if (parts.length <= 2) {
    return normalized;
  }

  const secondLevelTlds = new Set(["co.uk", "org.uk", "com.au", "net.au", "co.jp", "com.cn", "com.hk"]);
  const lastTwo = parts.slice(-2).join(".");
  const lastThree = parts.slice(-3).join(".");
  return secondLevelTlds.has(lastTwo) ? lastThree : lastTwo;
}

function isValidEmailCandidate(email) {
  const normalized = normalizeText(email).toLowerCase().replace(/^mailto:/i, "").split(/[?#]/)[0];
  if (!normalized || !/@/.test(normalized)) {
    return "";
  }

  const blockedSuffixes = [".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".pdf"];
  const blockedExactEmails = new Set([
    "johndoe@gmail.com",
    "john.doe@gmail.com",
    "example@example.com",
    "test@test.com"
  ]);

  if (blockedExactEmails.has(normalized)) {
    return "";
  }

  if (blockedSuffixes.some((suffix) => normalized.endsWith(suffix))) {
    return "";
  }

  if (
    /(example|johndoe|john\.doe|test|yourname|noreply|no-reply|donotreply|mailer-daemon|abuse|subscribe)/i.test(normalized.split("@")[0]) &&
    /(gmail\.com|yahoo\.com|hotmail\.com|outlook\.com|example\.com|domain\.com|email\.com)$/i.test(normalized)
  ) {
    return "";
  }

  if (isSuspiciousTechnicalEmail(normalized)) {
    return "";
  }

  return normalized;
}

function isGenericEmailProviderDomain(domain) {
  const rootDomain = getRootDomain(domain);
  return GENERIC_EMAIL_PROVIDER_ROOTS.has(rootDomain);
}

function scoreEmailCandidate(email, originHost, options = {}) {
  const normalized = isValidEmailCandidate(email);
  if (!normalized) {
    return Number.NEGATIVE_INFINITY;
  }

  const [localPart, domainPart] = normalized.split("@");
  const normalizedHost = normalizeText(originHost).toLowerCase().replace(/^www\./, "");
  const rootDomain = getRootDomain(normalizedHost);
  const emailRootDomain = getRootDomain(domainPart);
  let score = 0;

  if (domainPart === normalizedHost) {
    score += 140;
  } else if (rootDomain && emailRootDomain === rootDomain) {
    score += 110;
  }

  if (options.isMailto) {
    score += 30;
  }

  if (/^(info|contact|sales|hello|office|enquiry|enquiries|marketing|support|service|admin)$/i.test(localPart)) {
    score += 20;
  } else if (localPart.length <= 2) {
    score -= 10;
  }

  if (isGenericEmailProviderDomain(domainPart)) {
    score -= 10;
  }

  if (/^(noreply|no-reply|donotreply|mailer-daemon|abuse|subscribe)$/i.test(localPart)) {
    score -= 50;
  }

  return score;
}

function isSuspiciousTechnicalEmail(email) {
  const normalized = normalizeText(email).toLowerCase();
  if (!normalized.includes("@")) {
    return false;
  }

  const [localPart, domainPart] = normalized.split("@");
  if (!localPart || !domainPart) {
    return false;
  }

  const rootDomain = getRootDomain(domainPart);
  if (
    /(^|\.)sentry(?:[-.][a-z0-9-]+)*\./i.test(domainPart) ||
    SUSPICIOUS_EMAIL_PROVIDER_ROOTS.has(rootDomain)
  ) {
    return true;
  }

  return /^[a-f0-9-]{16,}$/i.test(localPart) && SUSPICIOUS_EMAIL_PROVIDER_ROOTS.has(rootDomain);
}

function decodeCloudflareEmail(encoded) {
  const value = normalizeText(encoded);
  if (!/^[0-9a-f]+$/i.test(value) || value.length < 4) {
    return "";
  }

  try {
    let output = "";
    const key = parseInt(value.slice(0, 2), 16);
    for (let index = 2; index < value.length; index += 2) {
      output += String.fromCharCode(parseInt(value.slice(index, index + 2), 16) ^ key);
    }

    return isValidEmailCandidate(output);
  } catch (_error) {
    return "";
  }
}

function extractLinkTargets(html, baseUrl) {
  const urls = new Set();
  const regex = /\b(?:href|data-href|data-url|content)=["']([^"'<>]+)["']/gi;
  let match;

  while ((match = regex.exec(html)) !== null) {
    const normalized = normalizeResolvableUrl(match[1], baseUrl);
    if (normalized) {
      urls.add(normalized);
    }
  }

  return [...urls];
}

async function fetchText(url) {
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), WEBSITE_TIMEOUT_MS);

  try {
    const response = await fetch(url, {
      redirect: "follow",
      signal: controller.signal
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`);
    }

    return await response.text();
  } finally {
    clearTimeout(timeoutId);
  }
}

async function mapWithConcurrency(items, limit, worker) {
  const results = [];
  let cursor = 0;

  async function run() {
    while (cursor < items.length) {
      const currentIndex = cursor;
      cursor += 1;
      results[currentIndex] = await worker(items[currentIndex], currentIndex);
    }
  }

  const workers = Array.from({ length: Math.min(limit, items.length) }, () => run());
  await Promise.all(workers);
  return results;
}

function extractEmailsFromText(text, originHost) {
  const matchMeta = new Map();
  const rootDomain = getRootDomain(originHost);

  function trackEmail(email, options = {}) {
    const normalized = isValidEmailCandidate(email);
    if (!normalized) {
      return;
    }

    const current = matchMeta.get(normalized) || {
      email: normalized,
      isMailto: false
    };
    if (options.isMailto) {
      current.isMailto = true;
    }
    matchMeta.set(normalized, current);
  }

  (text.match(/mailto:([^"'<>?\s]+)/gi) || []).forEach((mailto) => {
    trackEmail(mailto.replace(/^mailto:/i, ""), { isMailto: true });
  });

  const cloudflareRegex = /data-cfemail=["']([0-9a-f]+)["']/gi;
  let cloudflareMatch;
  while ((cloudflareMatch = cloudflareRegex.exec(text)) !== null) {
    trackEmail(decodeCloudflareEmail(cloudflareMatch[1]));
  }

  (text.match(EMAIL_REGEX) || []).forEach((email) => {
    trackEmail(email);
  });

  const ranked = [...matchMeta.values()].map((entry) => ({
    ...entry,
    score: scoreEmailCandidate(entry.email, originHost, { isMailto: entry.isMailto })
  })).filter((entry) => Number.isFinite(entry.score));

  const preferred = ranked.filter((entry) => {
    if (!rootDomain) {
      return false;
    }

    const emailDomain = entry.email.split("@")[1] || "";
    const emailRoot = getRootDomain(emailDomain);
    return emailDomain === originHost.replace(/^www\./, "") || emailRoot === rootDomain;
  });
  if (preferred.length) {
    return preferred
      .sort((left, right) => right.score - left.score || left.email.localeCompare(right.email))
      .map((entry) => entry.email)
      .slice(0, 4);
  }

  return ranked
    .filter((entry) => entry.isMailto || entry.score >= 15)
    .sort((left, right) => right.score - left.score || left.email.localeCompare(right.email))
    .map((entry) => entry.email)
    .slice(0, 4);
}

function normalizeSocialUrl(value) {
  const url = normalizeResolvableUrl(value);
  if (!url || SOCIAL_BLOCKLIST.test(url)) {
    return "";
  }

  try {
    const parsed = new URL(url);
    parsed.hash = "";

    ["fbclid", "gclid", "igshid", "si", "ref", "source", "utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"].forEach((key) => {
      parsed.searchParams.delete(key);
    });

    if (parsed.pathname.length > 1) {
      parsed.pathname = parsed.pathname.replace(/\/+$/, "");
    }

    return parsed.toString();
  } catch (_error) {
    return "";
  }
}

function extractSocialLinks(text, baseUrl = "") {
  const result = {};
  const candidates = new Set(extractLinkTargets(text, baseUrl));

  Object.entries(SOCIAL_PATTERNS).forEach(([key, pattern]) => {
    const matches = text.match(pattern) || [];
    matches.forEach((match) => {
      const normalized = normalizeSocialUrl(match);
      if (normalized) {
        candidates.add(normalized);
      }
    });
  });

  candidates.forEach((candidate) => {
    const normalized = normalizeSocialUrl(candidate);
    if (!normalized) {
      return;
    }

    if (!result.facebook && /facebook\.com|fb\.com/i.test(normalized)) {
      result.facebook = normalized;
    }
    if (!result.instagram && /instagram\.com|instagr\.am/i.test(normalized)) {
      result.instagram = normalized;
    }
    if (!result.linkedin && /linkedin\.com/i.test(normalized)) {
      result.linkedin = normalized;
    }
    if (!result.youtube && /youtube\.com|youtu\.be/i.test(normalized)) {
      result.youtube = normalized;
    }
    if (!result.twitter && /twitter\.com|x\.com/i.test(normalized)) {
      result.twitter = normalized;
    }
    if (!result.whatsapp && /wa\.me|whatsapp\.com/i.test(normalized)) {
      result.whatsapp = normalized;
    }
  });

  return result;
}

function extractCandidatePages(html, baseUrl) {
  const base = safeUrl(baseUrl);
  if (!base) {
    return [];
  }

  const pages = new Map();
  const keywords = new RegExp(DISCOVERY_PAGE_KEYWORDS.join("|"), "i");

  DISCOVERY_DEFAULT_PATHS.forEach((path, index) => {
    const candidate = new URL(path, base.origin);
    pages.set(candidate.toString(), 100 - index);
  });

  extractLinkTargets(html, base.toString()).forEach((url) => {
    const candidate = safeUrl(url);
    if (!candidate || candidate.origin !== base.origin) {
      return;
    }

    const haystack = `${candidate.pathname} ${candidate.search}`;
    if (!keywords.test(haystack)) {
      return;
    }

    const depth = candidate.pathname.split("/").filter(Boolean).length;
    const score = 80 - depth * 5;
    pages.set(candidate.toString(), Math.max(score, pages.get(candidate.toString()) || 0));
  });

  return [...pages.entries()]
    .sort((left, right) => right[1] - left[1])
    .map(([url]) => url)
    .slice(0, WEBSITE_MAX_PAGES);
}

function mergeSocials(target, source) {
  Object.entries(source || {}).forEach(([key, value]) => {
    if (value && !target[key]) {
      target[key] = value;
    }
  });
}

function cloneWebsiteEnrichmentResult(result) {
  return {
    emails: Array.isArray(result?.emails) ? [...result.emails] : [],
    socials: { ...(result?.socials || {}) }
  };
}

function pruneWebsiteEnrichmentCache() {
  if (websiteEnrichmentCache.size <= WEBSITE_ENRICH_CACHE_MAX_ENTRIES) {
    return;
  }

  const entries = [...websiteEnrichmentCache.entries()].sort((left, right) => left[1].cachedAt - right[1].cachedAt);
  const overflow = websiteEnrichmentCache.size - WEBSITE_ENRICH_CACHE_MAX_ENTRIES;
  entries.slice(0, overflow).forEach(([key]) => websiteEnrichmentCache.delete(key));
}

function getWebsiteEnrichmentCacheKey(url) {
  const target = safeUrl(url);
  return target ? target.origin : "";
}

function readWebsiteEnrichmentCache(cacheKey) {
  const cached = websiteEnrichmentCache.get(cacheKey);
  if (!cached) {
    return null;
  }

  if (Date.now() - cached.cachedAt > WEBSITE_ENRICH_CACHE_TTL_MS) {
    websiteEnrichmentCache.delete(cacheKey);
    return null;
  }

  return cloneWebsiteEnrichmentResult(cached.result);
}

function writeWebsiteEnrichmentCache(cacheKey, result) {
  if (!cacheKey) {
    return;
  }

  websiteEnrichmentCache.set(cacheKey, {
    cachedAt: Date.now(),
    result: cloneWebsiteEnrichmentResult(result)
  });
  pruneWebsiteEnrichmentCache();
}

async function enrichWebsiteUncached(target) {
  const visited = new Set();
  const queue = [...new Set([target.origin, target.toString()])];
  const emails = new Set();
  const socials = {};

  while (queue.length && visited.size < WEBSITE_MAX_PAGES) {
    const batch = [];

    while (
      queue.length &&
      batch.length < WEBSITE_FETCH_CONCURRENCY &&
      visited.size + batch.length < WEBSITE_MAX_PAGES
    ) {
      const current = queue.shift();
      if (!current || visited.has(current) || batch.includes(current)) {
        continue;
      }

      batch.push(current);
    }

    if (!batch.length) {
      break;
    }

    batch.forEach((current) => visited.add(current));

    const pages = await mapWithConcurrency(batch, WEBSITE_FETCH_CONCURRENCY, async (current) => {
      try {
        return {
          current,
          html: await fetchText(current)
        };
      } catch (_error) {
        return null;
      }
    });

    pages.forEach((page) => {
      if (!page?.html || !page.current) {
        return;
      }

      extractEmailsFromText(page.html, target.hostname).forEach((email) => emails.add(email));
      mergeSocials(socials, extractSocialLinks(page.html, page.current));
      extractCandidatePages(page.html, page.current).forEach((nextPage) => {
        if (!visited.has(nextPage) && !queue.includes(nextPage)) {
          queue.push(nextPage);
        }
      });
    });
  }

  return {
    emails: [...emails],
    socials
  };
}

async function enrichWebsite(url) {
  const target = safeUrl(url);
  if (!target) {
    return { emails: [], socials: {} };
  }

  const cacheKey = getWebsiteEnrichmentCacheKey(target.toString());
  const cached = readWebsiteEnrichmentCache(cacheKey);
  if (cached) {
    return cached;
  }

  if (websiteEnrichmentPending.has(cacheKey)) {
    return cloneWebsiteEnrichmentResult(await websiteEnrichmentPending.get(cacheKey));
  }

  const pendingPromise = enrichWebsiteUncached(target)
    .then((result) => {
      writeWebsiteEnrichmentCache(cacheKey, result);
      return result;
    })
    .finally(() => {
      websiteEnrichmentPending.delete(cacheKey);
    });

  websiteEnrichmentPending.set(cacheKey, pendingPromise);
  return cloneWebsiteEnrichmentResult(await pendingPromise);
}

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  (async () => {
    switch (message.action) {
      case "GET_LICENSE_STATE": {
        return { ok: true, license: await getLicenseState() };
      }

      case "REFRESH_LICENSE_STATE": {
        const license = await getLicenseState({ forceValidate: true });
        if (license.validationError) {
          return { ok: false, error: license.validationError, license };
        }
        return { ok: true, license };
      }

      case "ACTIVATE_LICENSE": {
        return { ok: true, license: await activateLicense(message.code) };
      }

      case "CLEAR_LICENSE": {
        await clearStoredLicense();
        return { ok: true, license: await getLicenseState() };
      }

      case "OPEN_LICENSE_PAGE": {
        await createTab(chrome.runtime.getURL("license.html"));
        return { ok: true };
      }

      case "OPEN_SEARCH": {
        const query = normalizeText(message.query);
        if (!query) {
          return { ok: false, error: "请输入搜索关键词。" };
        }

        await createTab(`https://www.google.com/maps/search/${encodeURIComponent(query)}`);

        return { ok: true };
      }

      case "START_COLLECTION_JOB": {
        await ensureLicenseActive();
        const query = normalizeText(message.query);
        if (!query) {
          return { ok: false, error: "请输入搜索关键词。" };
        }

        const job = await initializeCollectionSession(query, message.enrichEmail !== false);
        await setPendingJob(job);

        await createTab(`https://www.google.com/maps/search/${encodeURIComponent(query)}`);
        return { ok: true };
      }

      case "INIT_COLLECTION_SESSION": {
        await ensureLicenseActive();
        const query = normalizeText(message.query);
        if (!query) {
          return { ok: false, error: "请输入搜索关键词。" };
        }

        const job = await initializeCollectionSession(query, message.enrichEmail !== false);
        return { ok: true, job };
      }

      case "CLAIM_COLLECTION_JOB": {
        const job = await getPendingJob();
        if (!job) {
          return { ok: true, job: null };
        }

        const isExpired = Date.now() - Number(job.createdAt || 0) > 10 * 60 * 1000;
        if (isExpired) {
          await clearPendingJob();
          return { ok: true, job: null };
        }

        const senderMapsUrl = normalizeText(_sender?.tab?.url || _sender?.url);
        if (senderMapsUrl && !doesMapsUrlMatchCollectionQuery(senderMapsUrl, job.query)) {
          return { ok: true, job: null };
        }

        return { ok: true, job };
      }

      case "OPEN_DASHBOARD": {
        await ensureLicenseActive();
        const dashboardUrl = await ensureExtensionPageAvailable("dashboard.html");
        await createTab(dashboardUrl);
        return { ok: true, url: dashboardUrl };
      }

      case "GET_LEADS": {
        await ensureLicenseActive();
        return { ok: true, leads: await getStorage() };
      }

      case "GET_ACTIVE_COLLECTION_STATE": {
        await ensureLicenseActive();
        return { ok: true, collection: await getActiveCollection() };
      }

      case "UPDATE_ACTIVE_COLLECTION_STATE": {
        await ensureLicenseActive();
        const collection = await updateActiveCollectionState(message.collectionId, message.state || {});
        return { ok: true, collection };
      }

      case "CLEAR_LEADS": {
        await setStorage([]);
        await clearPendingJob();
        await setActiveCollection(null);
        return { ok: true };
      }

      case "MERGE_LEADS": {
        await ensureLicenseActive();
        const activeCollection = await getActiveCollection();
        const collectionId = normalizeText(message.collectionId);
        const currentLeads = await getStorage();

        if (!activeCollection?.collectionId || !collectionId || collectionId !== activeCollection.collectionId) {
          return {
            ok: true,
            count: currentLeads.length,
            leads: currentLeads,
            stats: {
              totalIncoming: Array.isArray(message.leads) ? message.leads.length : 0,
              insertedCount: 0,
              updatedCount: 0,
              unchangedCount: Array.isArray(message.leads) ? message.leads.length : 0
            }
          };
        }

        const merged = await mergeLeads(Array.isArray(message.leads) ? message.leads : []);
        return {
          ok: true,
          count: merged.leads.length,
          leads: merged.leads,
          stats: merged.stats
        };
      }

      case "ENRICH_WEBSITE": {
        await ensureLicenseActive();
        return { ok: true, ...(await enrichWebsite(message.url)) };
      }

      default:
        return { ok: false, error: "操作不存在。" };
    }
  })()
    .then((response) => sendResponse(response))
    .catch((error) => {
      sendResponse({
        ok: false,
        error: sanitizeUserFacingError(error),
        license: error.license || null
      });
    });

  return true;
});

chrome.action.onClicked.addListener(() => {
  openOrFocusLauncherWindow().catch(() => {
    // Ignore launcher open errors to avoid noisy runtime failures.
  });
});
