import { access } from "node:fs/promises";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
import { setTimeout as sleep } from "node:timers/promises";
import { fileURLToPath } from "node:url";

import dotenv from "dotenv";
import { chromium } from "playwright";

import { extractDetectedContacts, mergeDetectedContacts } from "./contact-utils.js";
import { antiDetectPause, attachVerificationMonitor, randomPause, stepPause, waitForVerificationClear } from "./human.js";

const SCRIPT_FILE = fileURLToPath(import.meta.url);
const SCRIPT_DIR = path.dirname(SCRIPT_FILE);
const PROJECT_ROOT = path.resolve(SCRIPT_DIR, "../..");
const AUTOMATION_DIR = path.join(PROJECT_ROOT, "automation");

function resolveProfileDir() {
  const explicit = process.env.SEARCH_1688_PROFILE_DIR ?? "";
  if (explicit) {
    return path.isAbsolute(explicit) ? explicit : path.resolve(PROJECT_ROOT, explicit);
  }
  return path.join(AUTOMATION_DIR, ".browser", "1688-profile");
}

dotenv.config({ path: path.join(PROJECT_ROOT, ".env") });

const PROFILE_DIR = resolveProfileDir();
const SEARCH_HOME_URL = "https://www.1688.com/";
const DEFAULT_OUTPUT_ROOT = path.join(AUTOMATION_DIR, "output", "image-search-keywords");
const DEFAULT_TIMEOUT_MS = Number.parseInt(process.env.SEARCH_TIMEOUT ?? "20000", 10);
const DEFAULT_RESULT_SAMPLE_COUNT = 20;
const DEFAULT_HEADLESS = (process.env.SEARCH_HEADLESS ?? "0") === "1";
const DEFAULT_CONTACT_LIMIT = Number.parseInt(process.env.SEARCH_CONTACT_LIMIT ?? "10", 10);
const DEFAULT_CONTACT_TIMEOUT_MS = Number.parseInt(process.env.SEARCH_CONTACT_TIMEOUT ?? "8000", 10);

const IMAGE_SEARCH_URL_PATTERN = /https?:\/\/(?:s|search|pre-search|1688-cbu01)\.1688\.com\/(?:youyuan\/index\.htm|factory\/image_search\.htm|wow\/cbu\/srch_rec\/image_search\/(?:test\/)?youyuan\/index\.html).*?[?&]tab=imageSearch\b/i;

function nowStamp() {
  return new Date().toISOString().replace(/[:.]/g, "-");
}

function formatTs() {
  return new Date().toLocaleString("zh-CN", { hour12: false });
}

function log(message) {
  console.log(`[${formatTs()}] ${message}`);
}

function applyProductLimit(items, limit) {
  if (!Array.isArray(items)) {
    return [];
  }

  return Number.isInteger(limit) && limit > 0 ? items.slice(0, limit) : items;
}

function resolveScanLimit(totalCount, limit) {
  if (!Number.isInteger(totalCount) || totalCount <= 0) {
    return 0;
  }

  return Number.isInteger(limit) && limit > 0 ? Math.min(totalCount, limit) : totalCount;
}

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

function normalizeToken(value) {
  return normalizeSpace(value).toLowerCase();
}

function dedupeList(values) {
  const seen = new Set();
  const output = [];

  for (const value of values) {
    const normalized = normalizeToken(value);
    if (!normalized || seen.has(normalized)) {
      continue;
    }

    seen.add(normalized);
    output.push(normalizeSpace(value));
  }

  return output;
}

async function ensureDir(dirPath) {
  await mkdir(dirPath, { recursive: true });
}

async function closeContextSafely(context, label = "context") {
  if (!context) {
    return;
  }

  for (const page of context.pages()) {
    await waitForVerificationClear(page, { logPrefix: `[${label}][关闭前验证码]` }).catch(() => false);
  }

  const timeoutMs = 5000;

  try {
    await Promise.race([
      context.close(),
      sleep(timeoutMs).then(() => {
        throw new Error(`${label}_close_timeout`);
      })
    ]);
  } catch (error) {
    log(`关闭浏览器上下文超时或失败（${label}）: ${error instanceof Error ? error.message : String(error)}`);
  }
}

async function closePageSafely(page, label = "page") {
  if (!page || page.isClosed()) {
    return;
  }

  await waitForVerificationClear(page, { logPrefix: `[${label}][关闭前验证码]` }).catch(() => false);

  const timeoutMs = 5000;
  await Promise.race([
    page.close().catch(() => {}),
    sleep(timeoutMs).then(() => {
      throw new Error(`${label}_close_timeout`);
    })
  ]).catch(() => {});
}

async function fileExists(filePath) {
  try {
    await access(filePath);
    return true;
  } catch {
    return false;
  }
}

function normalizeUrl(url) {
  if (!url) return "";
  if (url.startsWith("//")) return `https:${url}`;
  return url;
}

function looksLikeProductTitle(title) {
  const normalized = normalizeSpace(title);
  if (!normalized || normalized.length < 8) {
    return false;
  }

  if (/上传图片|搜索图片|帮你找同款|多选|已上传|ctrl \+ v|上传中/i.test(normalized)) {
    return false;
  }

  if (/入驻\d+年/.test(normalized)) {
    return false;
  }

  return true;
}

// 与 search-plan.js evaluateTextRules 相同的"无关商品占比"判断逻辑
function evaluateImageSearchTextRule(candidateProducts, coreTerms) {
  const total = candidateProducts.length;
  if (total === 0) {
    return { isValid: false, unrelated_ratio: 1, unrelated_count: 0, total: 0, reason: "no_results" };
  }
  if (coreTerms.length === 0) {
    return { isValid: true, unrelated_ratio: 0, unrelated_count: 0, total, reason: "no_filter_terms" };
  }
  let unrelatedCount = 0;
  for (const p of candidateProducts) {
    const lower = normalizeToken(p.title);
    if (!coreTerms.some((term) => lower.includes(term))) {
      unrelatedCount += 1;
    }
  }
  const unrelated_ratio = Number((unrelatedCount / total).toFixed(2));
  const isValid = unrelated_ratio <= 0.45;
  return { isValid, unrelated_ratio, unrelated_count: unrelatedCount, total, reason: isValid ? "text_rule_passed" : "text_rule_rejected" };
}

// 与 search-plan.js collectImageSimilarity 相同的 perceptual hash 聚类逻辑
async function collectImageHashSimilarity(page, sampleCount = 18) {
  // 优先用与 extractOfferResults 一致的卡片选择器（图搜页 href 属性原始值可能不含 offerId=）
  // 回退到通用 offer 链接选择器（文字搜索页）
  const cardImgLocator = page.locator('[class*="searchOfferWrapper"][class*="searchOfferItem"] img');
  const cardImgCount = await cardImgLocator.count();
  const imageLocators = cardImgCount > 0
    ? cardImgLocator
    : page.locator('a[href*="offerId="] img, a[href*="detail.1688.com/offer/"] img');
  const total = Math.min(await imageLocators.count(), sampleCount);
  const snapshots = [];

  for (let index = 0; index < total; index += 1) {
    const locator = imageLocators.nth(index);
    const visible = await locator.isVisible().catch(() => false);
    if (!visible) {
      continue;
    }
    try {
      const buffer = await locator.screenshot({ type: "png", animations: "disabled" });
      snapshots.push(`data:image/png;base64,${buffer.toString("base64")}`);
    } catch {
      // 单张缩略图失败不影响整体判断
    }
  }

  if (snapshots.length < 4) {
    return { isValid: false, sample_count: snapshots.length, dominant_cluster_ratio: 0, reason: "insufficient_image_samples" };
  }

  const hashes = await page.evaluate(async (dataUrls) => {
    function hammingDistance(left, right) {
      let distance = 0;
      for (let index = 0; index < Math.min(left.length, right.length); index += 1) {
        if (left[index] !== right[index]) {
          distance += 1;
        }
      }
      return distance + Math.abs(left.length - right.length);
    }

    async function computeHash(dataUrl) {
      return new Promise((resolve, reject) => {
        const image = new Image();
        image.onload = () => {
          const canvas = document.createElement("canvas");
          canvas.width = 8;
          canvas.height = 8;
          const ctx = canvas.getContext("2d", { willReadFrequently: true });
          if (!ctx) {
            reject(new Error("canvas_context_unavailable"));
            return;
          }
          ctx.drawImage(image, 0, 0, 8, 8);
          const { data } = ctx.getImageData(0, 0, 8, 8);
          const grayValues = [];
          for (let index = 0; index < data.length; index += 4) {
            grayValues.push(Math.round(data[index] * 0.299 + data[index + 1] * 0.587 + data[index + 2] * 0.114));
          }
          const average = grayValues.reduce((sum, value) => sum + value, 0) / grayValues.length;
          resolve(grayValues.map((value) => (value >= average ? "1" : "0")).join(""));
        };
        image.onerror = () => reject(new Error("image_decode_failed"));
        image.src = dataUrl;
      });
    }

    const output = [];
    for (const dataUrl of dataUrls) {
      try {
        output.push(await computeHash(dataUrl));
      } catch {
        // ignore
      }
    }
    return { hashes: output, hammingDistanceSource: hammingDistance.toString() };
  }, snapshots);

  const hammingDistanceFn = new Function(`return (${hashes.hammingDistanceSource});`)();
  let bestClusterSize = 0;
  for (const leftHash of hashes.hashes) {
    const clusterSize = hashes.hashes.filter((rightHash) => hammingDistanceFn(leftHash, rightHash) <= 10).length;
    bestClusterSize = Math.max(bestClusterSize, clusterSize);
  }

  const dominantClusterRatio = hashes.hashes.length > 0 ? bestClusterSize / hashes.hashes.length : 0;
  const isValid = dominantClusterRatio >= 0.35;
  return {
    isValid,
    sample_count: hashes.hashes.length,
    dominant_cluster_ratio: Number(dominantClusterRatio.toFixed(2)),
    reason: isValid ? "image_similarity_passed" : "image_similarity_rejected"
  };
}

function isRealImageSearchUrl(url) {
  return IMAGE_SEARCH_URL_PATTERN.test(normalizeUrl(url));
}

async function waitForOfferLinkSignals(page, timeoutMs) {
  await page.waitForFunction(() => {
    const isVisible = (element) => {
      const rect = element.getBoundingClientRect();
      return rect.width > 0 && rect.height > 0;
    };

    return Array.from(document.querySelectorAll('a[href]')).some((link) => {
      const href = link.getAttribute('href') || '';
      return isVisible(link) && (href.includes('offerId=') || href.includes('detail.1688.com/offer/'));
    });
  }, { timeout: timeoutMs });
}

async function captureWindowOpenUrls(page) {
  await page.evaluate(() => {
    const openCalls = [];
    const originalOpen = window.open.bind(window);

    window.__imageSearchOpenCalls = openCalls;
    window.open = (...args) => {
      openCalls.push({
        url: typeof args[0] === 'string' ? args[0] : '',
        target: typeof args[1] === 'string' ? args[1] : ''
      });
      return originalOpen(...args);
    };
  });
}

async function getCapturedWindowOpenUrl(page) {
  const openCalls = await page.evaluate(() => window.__imageSearchOpenCalls || []);
  const matched = [...openCalls].reverse().find((entry) => isRealImageSearchUrl(entry.url));
  return normalizeUrl(matched?.url || '');
}

async function resolveImageSearchResultPage(context, page, popupPage, timeoutMs) {
  if (popupPage) {
    await popupPage.waitForLoadState('domcontentloaded', { timeout: timeoutMs }).catch(() => {});
    if (isRealImageSearchUrl(popupPage.url())) {
      await waitForOfferLinkSignals(popupPage, timeoutMs);
      return popupPage;
    }
  }

  if (isRealImageSearchUrl(page.url())) {
    await waitForOfferLinkSignals(page, timeoutMs);
    return page;
  }

  const openedUrl = await getCapturedWindowOpenUrl(page);
  if (openedUrl) {
    const resultPage = await context.newPage();
    await resultPage.goto(openedUrl, { waitUntil: 'domcontentloaded', timeout: timeoutMs });
    await waitForOfferLinkSignals(resultPage, timeoutMs);
    return resultPage;
  }

  throw new Error('点击“搜索图片”后未进入真正的 1688 图搜结果页');
}
function normalizeProductRecord(item, fallbackIndex) {
  return {
    index: Number(item?.index ?? fallbackIndex),
    title: normalizeSpace(item?.title ?? ""),
    offer_url: String(item?.offerUrl ?? item?.offer_url ?? ""),
    image_url: normalizeUrl(String(item?.imageUrl ?? item?.image_url ?? "")),
    merchant_name: normalizeSpace(String(item?.merchantName ?? item?.merchant_name ?? "")),
    merchant_url: String(item?.merchantUrl ?? item?.merchant_url ?? ""),
    wangwang_url: String(item?.wangwang_url ?? ""),
    detected_contacts: [],
    contact_page_url: ""
  };
}

async function collectPageText(page) {
  const payload = await page.evaluate(() => ({
    bodyText: document.body?.innerText ?? "",
    hrefs: Array.from(document.querySelectorAll("a[href]")).map((n) => n.getAttribute("href") ?? "").filter(Boolean),
    tels: Array.from(document.querySelectorAll('a[href^="tel:"], a[href^="mailto:"]')).map((n) => n.getAttribute("href") ?? "").filter(Boolean)
  }));
  return [payload.bodyText, ...payload.hrefs, ...payload.tels].join("\n");
}

async function extractMerchantInfoFromOfferPage(page) {
  return page.evaluate(() => {
    function clean(text) {
      return (text ?? "").replace(/\s+/g, " ").trim();
    }

    const merchantName = clean(
      document.querySelector('.shop-company-name, [class*="shop-company-name"]')?.textContent
      || document.querySelector('[class*="shopName"]')?.textContent
      || ""
    );

    const selectors = [
      'a.shop-company-name[href]',
      'a[class*="shop-company-name"][href]',
      'a.shop-container[href]',
      'a[class*="shop-container"][href]',
      'a[href*=".1688.com/?offerId="][class*="shop"]'
    ];

    let merchantAnchor = null;
    for (const selector of selectors) {
      const matched = document.querySelector(selector);
      if (matched instanceof HTMLAnchorElement) {
        merchantAnchor = matched;
        break;
      }
    }

    if (!merchantAnchor) {
      merchantAnchor = Array.from(document.querySelectorAll('a[href]')).find((link) => {
        const href = link instanceof HTMLAnchorElement ? link.href : "";
        const text = clean(link.textContent);
        return /\.1688\.com/i.test(href)
          && !/detail\.1688\.com\/offer\//i.test(href)
          && !/def_cbu_web_im/i.test(href)
          && Boolean(text);
      }) ?? null;
    }

    return {
      merchantName,
      merchantUrl: merchantAnchor instanceof HTMLAnchorElement ? merchantAnchor.href : ""
    };
  });
}

function buildContactPageUrl(merchantUrl) {
  const base = normalizeUrl(merchantUrl);
  if (!base) return "";
  try {
    const parsed = new URL(base);
    return `${parsed.origin}/page/contactinfo.htm`;
  } catch {
    const withoutQuery = base.split(/[?#]/, 1)[0] ?? "";
    const originMatch = withoutQuery.match(/^(https?:\/\/[^/]+)/i);
    if (originMatch?.[1]) {
      return `${originMatch[1]}/page/contactinfo.htm`;
    }
    return withoutQuery.endsWith("/")
      ? withoutQuery + "page/contactinfo.htm"
      : withoutQuery + "/page/contactinfo.htm";
  }
}

async function scanContactsFromUrl(context, url, source, timeoutMs) {
  const normalizedUrl = normalizeUrl(url);
  if (!normalizedUrl) {
    return { detectedContacts: [], merchantName: "", merchantUrl: "" };
  }
  const page = await context.newPage();
  try {
    await waitForVerificationClear(page, { logPrefix: "[image-contact][导航前验证码]" });
    const response = await page.goto(normalizedUrl, { waitUntil: "domcontentloaded", timeout: timeoutMs }).catch(() => null);
    await waitForVerificationClear(page, { logPrefix: "[image-contact][验证码]" });
    await page.waitForLoadState("domcontentloaded", { timeout: timeoutMs }).catch(() => {});
    await stepPause("image_contact_page_ready");
    if (isLoginUrl(page.url()) || (response && !response.ok())) {
      return { detectedContacts: [], merchantName: "", merchantUrl: "" };
    }

    const text = await collectPageText(page);
    const merchantInfo = source === "product_detail"
      ? await extractMerchantInfoFromOfferPage(page).catch(() => ({ merchantName: "", merchantUrl: "" }))
      : { merchantName: "", merchantUrl: "" };

    return {
      detectedContacts: extractDetectedContacts(text, source),
      merchantName: merchantInfo.merchantName,
      merchantUrl: merchantInfo.merchantUrl
    };
  } catch {
    return { detectedContacts: [], merchantName: "", merchantUrl: "" };
  } finally {
    await closePageSafely(page, "image_contact");
  }
}
async function promptForManualLogin(page, targetUrl, options = {}) {
  if (options?.headless || !input.isTTY || !output.isTTY) {
    const mode = options?.headless ? "无界面" : "非交互";
    throw new Error(
      `检测到 1688 登录页，但当前为${mode}模式，无法手工登录。请先运行 automation/internal/login-1688.js 完成登录并写入 ${PROFILE_DIR}，然后重试图搜。`
    );
  }

  log("检测到需要登录，请在浏览器里完成登录后回车继续。");
  const rl = readline.createInterface({ input, output });
  await rl.question("登录完成后按回车继续: ");
  rl.close();
  await waitForVerificationClear(page, { logPrefix: "[image-login][返回前验证码]" }).catch(() => false);
  await page.goto(targetUrl, { waitUntil: "domcontentloaded" });
  await waitForVerificationClear(page, { logPrefix: "[image-login][返回后验证码]" }).catch(() => false);
}

function isLoginUrl(url) {
  return /login|passport|auth/i.test(String(url ?? ""));
}

function parseArgs(argv) {
  const options = {
    runSummaryPath: "",
    imagePath: "",
    outputRoot: DEFAULT_OUTPUT_ROOT,
    materialId: "",
    resultSampleCount: DEFAULT_RESULT_SAMPLE_COUNT,
    timeoutMs: DEFAULT_TIMEOUT_MS,
    headless: DEFAULT_HEADLESS,
    contactLimit: DEFAULT_CONTACT_LIMIT,
    keywordProcessPath: ""
  };

  for (const arg of argv) {
    if (!arg.startsWith("--")) continue;
    const [flag, rawValue] = arg.slice(2).split("=", 2);
    const value = rawValue ?? "";

    if (flag === "run-summary" && value) {
      options.runSummaryPath = path.resolve(process.cwd(), value);
    } else if (flag === "image" && value) {
      options.imagePath = path.resolve(process.cwd(), value);
    } else if (flag === "output" && value) {
      options.outputRoot = path.resolve(process.cwd(), value);
    } else if (flag === "material-id" && value) {
      options.materialId = value;
    } else if (flag === "sample-count" && value) {
      options.resultSampleCount = Number.parseInt(value, 10);
    } else if (flag === "contact-limit" && value) {
      options.contactLimit = Number.parseInt(value, 10);
    } else if (flag === "timeout-ms" && value) {
      options.timeoutMs = Number.parseInt(value, 10);
    } else if (flag === "headless" && value) {
      options.headless = value === "1" || value === "true";
    } else if (flag === "keyword-process" && value) {
      options.keywordProcessPath = path.resolve(process.cwd(), value);
    }
  }

  return options;
}

async function resolveImagePath(options) {
  if (options.imagePath) {
    return {
      imagePath: options.imagePath,
      source: "explicit_image",
      bestMatch: null
    };
  }

  if (!options.runSummaryPath) {
    throw new Error("必须提供 --image 或 --run-summary");
  }

  const rawText = await readFile(options.runSummaryPath, "utf8");
  const summary = JSON.parse(rawText);
  const bestImages = Array.isArray(summary.best_match_images) ? summary.best_match_images : [];
  const matched = options.materialId
    ? bestImages.find((item) => String(item.material_id ?? "") === options.materialId)
    : bestImages[0];

  if (!matched?.image_path) {
    throw new Error("run-summary 中没有可用的 best_match_images.image_path");
  }

  return {
    imagePath: matched.image_path,
    source: "run_summary_best_match",
    bestMatch: matched
  };
}

async function openImageSearchResults(context, imagePath, timeoutMs, options = {}) {
  const page = await context.newPage();
  await waitForVerificationClear(page, { logPrefix: "[image-search][首页前验证码]" });
  await page.goto(SEARCH_HOME_URL, { waitUntil: "domcontentloaded", timeout: timeoutMs });
  await waitForVerificationClear(page, { logPrefix: "[image-search][首页验证码]" });
  await stepPause("image_search_home_ready");

  if (isLoginUrl(page.url())) {
    await promptForManualLogin(page, SEARCH_HOME_URL, options);
  }

  await page.waitForSelector("input[type=file].image-file-reader-wrapper", { timeout: timeoutMs });
  await stepPause("image_upload_ready");
  await page.setInputFiles("input[type=file].image-file-reader-wrapper", imagePath);

  await page.waitForFunction(() => {
    const panel = document.querySelector('.copy-image-container, .search-image-upload-container, .image-input-button');
    return Boolean(panel && /已上传|帮你找同款|搜索图片/.test(panel.textContent || ''));
  }, { timeout: timeoutMs });

  const submitButton = page.locator('.copy-image-container .search-btn').first();
  await submitButton.waitFor({ state: 'visible', timeout: timeoutMs });
  await captureWindowOpenUrls(page);

  const popupPromise = context.waitForEvent('page', { timeout: timeoutMs }).catch(() => null);
  const samePagePromise = page.waitForURL((url) => isRealImageSearchUrl(url.toString()), { timeout: timeoutMs })
    .then(() => page)
    .catch(() => null);

  await submitButton.click();

  const [popupPage] = await Promise.all([
    popupPromise,
    samePagePromise,
    stepPause("image_search_submitted")
  ]);

  return resolveImageSearchResultPage(context, page, popupPage, timeoutMs);
}

async function extractOfferResults(page, limit) {
  await page.waitForFunction(() => {
    const isVisible = (element) => {
      const rect = element.getBoundingClientRect();
      return rect.width > 0 && rect.height > 0;
    };

    const hasOfferLink = Array.from(document.querySelectorAll('a[href]')).some((link) => {
      const href = link.getAttribute('href') || '';
      return isVisible(link) && (href.includes('offerId=') || href.includes('detail.1688.com/offer/'));
    });

    if (hasOfferLink) {
      return true;
    }

    return Array.from(document.querySelectorAll('a[href], img')).some((node) => {
      const host = node.closest('a, li, div');
      if (!host || !isVisible(host)) {
        return false;
      }

      const text = (host.textContent || '').replace(/\s+/g, ' ').trim();
      const image = host.tagName === 'IMG' ? host : host.querySelector('img');
      return Boolean(image) && text.length >= 6;
    });
  }, { timeout: 30000 });
  await page.mouse.wheel(0, 2400).catch(() => {});
  await stepPause("image_results_scrolled");

  const rawResults = await page.evaluate((take) => {
    const cardResults = [];
    const cardSeen = new Set();

    function clean(text) {
      return (text ?? "").replace(/\s+/g, " ").trim();
    }

    function normalizeMerchantName(text) {
      return clean(text)
        .replace(/入驻\d+年/g, " ")
        .replace(/回头率\d+(?:\.\d+)?%/g, " ")
        .replace(/店铺服务分\s*\d+(?:\.\d+)?分/g, " ")
        .replace(/准时发货率\s*\d+(?:\.\d+)?%/g, " ")
        .replace(/店铺好评率\s*\d+(?:\.\d+)?%/g, " ")
        .replace(/主营[:：][^\s]+/g, " ")
        .replace(/旺旺在线/g, " ");
    }

    function pickMerchantName(card) {
      const direct = clean(
        card.querySelector('[class*="shopName"]')?.textContent
        || card.querySelector('[class*="shop-company-name"]')?.textContent
        || ""
      );

      if (direct) {
        return direct;
      }

      const shopRowText = clean(
        card.querySelector('[class*="offerShopRow"]')?.textContent
        || card.querySelector('[class*="imageSearchShopInfo"]')?.textContent
        || ""
      );

      return normalizeMerchantName(shopRowText);
    }

    function normalizeTitle(text) {
      return clean(text)
        .replace(/^找相似/g, "")
        .replace(/首单减[^¥]*?/g, "")
        .replace(/分享再减[^¥]*?/g, "")
        .replace(/元宝可抵[^¥]*?/g, "")
        .replace(/¥\s*\d[\d.]*[\s\S]*$/g, "")
        .replace(/(材料|型式|造型|品种|类型)[:：][\s\S]*$/g, "")
        .replace(/入驻\d+年[\s\S]*$/g, "")
        .trim();
    }

    const cardNodes = Array.from(document.querySelectorAll('[class*="searchOfferWrapper"][class*="searchOfferItem"]'));

    for (const card of cardNodes) {
      const text = clean(card.textContent || "");
      const title = normalizeTitle(text);
      const image = card.querySelector('img');
      const offerAnchor = Array.from(card.querySelectorAll('a[href]')).find((link) => /offerId=\d+/.test(link.href));
      const offerIdMatch = offerAnchor?.href?.match(/offerId=(\d+)/);
      const offerId = offerIdMatch?.[1] || "";
      const dedupeKey = offerId || title;

      if (!dedupeKey || cardSeen.has(dedupeKey) || !title || title.length < 8) {
        continue;
      }

      cardSeen.add(dedupeKey);
      const merchantAnchor = Array.from(card.querySelectorAll('a[href]')).find((link) => {
        const href = link.getAttribute('href') || '';
        return href.includes('.1688.com') && !/offerId=\d+/.test(href) && !href.includes('detail.1688.com/offer/');
      });

      cardResults.push({
        title,
        offerUrl: offerId ? `https://detail.1688.com/offer/${offerId}.html` : offerAnchor?.href || '',
        imageUrl:
          image?.getAttribute('src') ??
          image?.getAttribute('data-src') ??
          image?.getAttribute('data-lazy-src') ??
          '',
        merchantName: pickMerchantName(card),
        merchantUrl: merchantAnchor?.href || ''
      });

      if (cardResults.length >= take) {
        return cardResults;
      }
    }

    if (cardResults.length > 0) {
      return cardResults;
    }

    const anchors = Array.from(document.querySelectorAll("a[href]"));
    const items = [];
    const seen = new Set();

    function isOfferLink(link) {
      return link.href.includes("detail.m.1688.com/page/index.html") || link.href.includes("detail.1688.com/offer/");
    }

    function isVisible(element) {
      const rect = element.getBoundingClientRect();
      return rect.width > 0 && rect.height > 0;
    }

    for (const link of anchors) {
      if (!isVisible(link) || !isOfferLink(link) || seen.has(link.href)) {
        continue;
      }

      seen.add(link.href);
      const image = link.querySelector("img");
      const title = clean(link.textContent);
      if (!title || title.length < 6) {
        continue;
      }

      items.push({
        title,
        offerUrl: link.href,
        imageUrl:
          image?.getAttribute("src") ??
          image?.getAttribute("data-src") ??
          image?.getAttribute("data-lazy-src") ??
          ""
      });

      if (items.length >= take) {
        return items;
      }
    }

    const cards = Array.from(document.querySelectorAll('img'));

    for (const image of cards) {
      const host = image.closest('a, li, div');
      if (!host || !isVisible(host)) {
        continue;
      }

      const link = image.closest('a[href]') || host.querySelector('a[href]');
      const offerUrl = link?.href || '';
      const dedupeKey = offerUrl || image.getAttribute('src') || image.getAttribute('data-src') || '';
      if (!dedupeKey || seen.has(dedupeKey)) {
        continue;
      }

      const title = clean(image.getAttribute('alt')) || clean(image.getAttribute('title')) || clean(host.textContent);
      if (!title || title.length < 6) {
        continue;
      }

      seen.add(dedupeKey);
      const merchantLink = Array.from(host.querySelectorAll('a[href]')).find((anchor) => {
        return anchor !== link && anchor.href.includes('.1688.com') && !anchor.href.includes('detail.1688.com/offer/');
      });

      items.push({
        title,
        offerUrl,
        imageUrl:
          image.getAttribute('src') ??
          image.getAttribute('data-src') ??
          image.getAttribute('data-lazy-src') ??
          '',
        merchantName: clean(merchantLink?.textContent),
        merchantUrl: merchantLink?.href || ''
      });

      if (items.length >= take) {
        return items;
      }
    }

    return items;
  }, limit);

  return rawResults
    .map((item, index) => ({
      index: index + 1,
      title: normalizeSpace(item.title),
      offerUrl: item.offerUrl,
      imageUrl: normalizeUrl(item.imageUrl),
      merchantName: normalizeSpace(item.merchantName),
      merchantUrl: normalizeUrl(item.merchantUrl)
    }))
    .filter((item) => item.offerUrl && looksLikeProductTitle(item.title));
}

async function savePageScreenshot(page, targetPath) {
  await page.screenshot({ path: targetPath, type: "jpeg", quality: 70, fullPage: true });
  return targetPath;
}

async function main() {
  const options = parseArgs(process.argv.slice(2));
  const resolvedImage = await resolveImagePath(options);
  const outputDir = path.join(options.outputRoot, `run-${nowStamp()}`);
  await ensureDir(outputDir);
  await ensureDir(PROFILE_DIR);

  let context = await chromium.launchPersistentContext(PROFILE_DIR, {
    channel: "chrome",
    headless: options.headless,
    locale: "zh-CN",
    viewport: { width: 1440, height: 960 },
    args: ["--disable-blink-features=AutomationControlled"]
  });
  const verificationMonitor = attachVerificationMonitor(context, { logPrefix: "[image-search][验证码]" });

  const report = {
    report_type: "1688_image_search_keyword_report",
    started_at: new Date().toISOString(),
    source_image_path: resolvedImage.imagePath,
    source: resolvedImage.source,
    material_id: resolvedImage.bestMatch?.material_id ?? options.materialId ?? null,
    output_dir: outputDir,
    image_search: null,
    similar_products: []
  };

  try {
    log(`使用图片: ${resolvedImage.imagePath}`);
    const imageSearchPage = await openImageSearchResults(context, resolvedImage.imagePath, options.timeoutMs, options);
    const imageSearchScreenshotPath = await savePageScreenshot(imageSearchPage, path.join(outputDir, "image-search-page.jpg"));
    const imageSearchResults = await extractOfferResults(imageSearchPage, options.resultSampleCount);
    const candidateProducts = imageSearchResults.slice(0, options.resultSampleCount);

    // 1. 从 keyword-process.json 提取核心词，过滤明显不相关的商品
    const coreTerms = [];
    if (options.keywordProcessPath) {
      try {
        const kp = JSON.parse(await readFile(options.keywordProcessPath, "utf8"));
        const suggested = String(kp?.ai_keyword_analysis?.suggested_keyword ?? "");
        const material = String(kp?.llm_analysis?.material ?? "");
        const altTerms = Array.isArray(kp?.llm_analysis?.alternative_terms)
          ? kp.llm_analysis.alternative_terms
          : [];
        const rawTerms = [suggested, material, ...altTerms].join(" ");
        for (const t of rawTerms.split(/[,\s、，]+/)) {
          const trimmed = t.trim();
          if (trimmed.length >= 2) coreTerms.push(trimmed.toLowerCase());
        }
      } catch {
        // keyword-process.json 不可读时跳过过滤
      }
    }

    function isTitleRelevant(title) {
      if (coreTerms.length === 0) return true;
      const lower = normalizeToken(title);
      return coreTerms.some((term) => lower.includes(term));
    }

    // 2. 按商家去重（merchantUrl 优先，其次 merchantName），两者皆空则保留
    const seenMerchants = new Set();
    function isMerchantFresh(product) {
      const key = normalizeToken(product.merchantUrl || product.merchantName || "");
      if (!key) return true;
      if (seenMerchants.has(key)) return false;
      seenMerchants.add(key);
      return true;
    }

    const similarProducts = candidateProducts
      .filter((p) => isTitleRelevant(p.title))
      .filter((p) => isMerchantFresh(p))
      ;
    const limitedSimilarProducts = applyProductLimit(similarProducts, options.contactLimit);

    if (coreTerms.length > 0) {
      const filtered = candidateProducts.length - limitedSimilarProducts.length;
      if (filtered > 0) log(`商品过滤: 从 ${candidateProducts.length} 条中过滤 ${filtered} 条不相关或重复商家商品，保留 ${limitedSimilarProducts.length} 条`);
    }

    // 图搜结果有效性判断（与文字搜索相同逻辑：文本规则 + 图片哈希相似度）
    const imageSearchTextRule = evaluateImageSearchTextRule(candidateProducts, coreTerms);
    let imageSearchSimilarity = { isValid: false, sample_count: 0, dominant_cluster_ratio: 0, reason: "not_run" };
    try {
      imageSearchSimilarity = await collectImageHashSimilarity(imageSearchPage);
    } catch {
      imageSearchSimilarity = { isValid: false, sample_count: 0, dominant_cluster_ratio: 0, reason: "hash_error" };
    }
    const isValidImageSearch = imageSearchTextRule.isValid && imageSearchSimilarity.isValid;
    log(`图搜有效性: ${isValidImageSearch ? "有效" : "无效"} | 文本=${imageSearchTextRule.reason}(无关率=${imageSearchTextRule.unrelated_ratio}) | 图片=${imageSearchSimilarity.reason}(主类聚比=${imageSearchSimilarity.dominant_cluster_ratio})`);

    report.image_search = {
      final_url: imageSearchPage.url(),
      screenshot_path: imageSearchScreenshotPath,
      extracted_result_count: imageSearchResults.length,
      similar_product_count: limitedSimilarProducts.length,
      top_titles: limitedSimilarProducts.map((item) => item.title),
      is_valid_image_search: isValidImageSearch,
      text_rule: imageSearchTextRule,
      image_similarity: imageSearchSimilarity
    };
    const normalizedProducts = limitedSimilarProducts.map((item, i) => normalizeProductRecord(item, i + 1));
    await closePageSafely(imageSearchPage, "image_search_results");

    const scanLimit = resolveScanLimit(normalizedProducts.length, options.contactLimit);
    if (scanLimit > 0) {
      log(`开始爬取图搜相似商品联系方式 (${scanLimit === normalizedProducts.length ? "全量" : `最多 ${scanLimit} 个商品`})`);
      for (const product of applyProductLimit(normalizedProducts, scanLimit)) {
        const detailScan = await scanContactsFromUrl(context, product.offer_url, "product_detail", DEFAULT_CONTACT_TIMEOUT_MS);
        await antiDetectPause();

        if (!product.merchant_name && detailScan.merchantName) {
          product.merchant_name = detailScan.merchantName;
        }
        if (!product.merchant_url && detailScan.merchantUrl) {
          product.merchant_url = detailScan.merchantUrl;
        }

        const contactPageUrl = buildContactPageUrl(product.merchant_url);
        const contactPageScan = await scanContactsFromUrl(context, contactPageUrl, "contact_page", DEFAULT_CONTACT_TIMEOUT_MS);
        await antiDetectPause();
        product.contact_page_url = contactPageUrl;
        product.detected_contacts = mergeDetectedContacts(detailScan.detectedContacts, contactPageScan.detectedContacts);
      }
    }

    report.similar_products = normalizedProducts;
    report.finished_at = new Date().toISOString();

    const reportPath = path.join(outputDir, "image-search-keyword-report.json");
    await writeFile(reportPath, JSON.stringify(report, null, 2), "utf8");
    log(`图搜报告已生成: ${reportPath}`);
  } finally {
    verificationMonitor.dispose();
    if (context) {
      await closeContextSafely(context, "image_search");
    }
  }
}

main()
  .then(() => {
    process.exit(0);
  })
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });
