/**
 * run-supabase.js
 *
 * 集成 Supabase 输入/输出的 1688 采购自动化流程。
 * 基于 run-1688.js 的成熟搜索 & 旺旺聊天逻辑，增加：
 *   - 输入：从 Supabase procurement_materials 表读取关键词
 *   - 输出：搜索结果 → final_result_products 表、图片 → Storage、联系记录 → contact_logs 表
 *
 * 用法：从项目根目录执行
 *   node playwright_1688-main/src/run-supabase.js
 *
 * 环境变量 (.env)：
 *   SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY (必填，兼容 VITE_SUPABASE_SERVICE_ROLE_KEY)
 *   RESULT_LIMIT        搜索结果数量上限 (默认 10)
 *   CONTACT_MERCHANTS   是否联系商家 (默认 1，设0跳过)
 *   HEADLESS            无头模式 (默认 0)
 *   KEEP_BROWSER_OPEN   运行完保持浏览器 (默认 0)
 *   ASK_MESSAGE          自定义消息 (留空则根据材料信息自动生成)
 */

import { mkdir } from "node:fs/promises";
import { existsSync } from "node:fs";
import path from "node:path";
import process from "node:process";
import crypto from "node:crypto";
import readline from "node:readline/promises";
import { stdin as input, stdout as output } from "node:process";
import { fileURLToPath } from "node:url";
import { chromium } from "playwright";

import {
  antiDetectPause,
  attachVerificationMonitor,
  humanClick,
  humanScrollPage,
  humanType,
  randomBetween,
  randomPause,
} from "./human.js";
import { normalizeOfferRecord } from "./offer-normalizer.js";
import { createResolvedSupabaseClient } from "./supabase-client.js";
import { build1688OfferSearchUrl } from "../../automation/playwright/search-results-url-utils.js";

// ── 路径 ─────────────────────────────────────────────
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PLAYWRIGHT_DIR = path.resolve(__dirname, "..");
const WORKSPACE_ROOT = process.cwd();

// 加载 .env（优先 cwd，其次 playwright_1688-main 上级目录）
const envPaths = [
  path.resolve(WORKSPACE_ROOT, ".env"),
  path.resolve(PLAYWRIGHT_DIR, "..", ".env"),
];
for (const p of envPaths) {
  if (existsSync(p)) {
    const dotenv = await import("dotenv");
    dotenv.config({ path: p });
    break;
  }
}

// ── 配置 ─────────────────────────────────────────────
const PROFILE_DIR = path.join(PLAYWRIGHT_DIR, ".browser", "1688-profile");
const RESULT_LIMIT = Number.parseInt(process.env.RESULT_LIMIT ?? "10", 10);
const HEADLESS = process.env.HEADLESS === "1";
const CONTACT_MERCHANTS = process.env.CONTACT_MERCHANTS !== "0";
const KEEP_BROWSER_OPEN = process.env.KEEP_BROWSER_OPEN === "1";
const CUSTOM_ASK_MESSAGE = process.env.ASK_MESSAGE ?? "";
const STORAGE_BUCKET = "1688_screenshots";

// ══════════════════════════════════════════════════════
//  Supabase
// ══════════════════════════════════════════════════════

function initSupabase() {
  return createResolvedSupabaseClient(process.env);
}

async function fetchMaterials(supabase) {
  const { data, error } = await supabase
    .from("procurement_materials")
    .select("id, batch_id, material_name, specification, quantity, unit, brand, technical_standard, material_requirement")
    .order("created_at", { ascending: true });

  if (error) throw new Error(`读取 procurement_materials 失败: ${error.message}`);
  if (!data || data.length === 0) throw new Error("procurement_materials 表为空，请先运行预处理 pnpm --dir chatbot run preprocess");

  return data;
}

function composeAskMessage(material) {
  if (CUSTOM_ASK_MESSAGE) return CUSTOM_ASK_MESSAGE;

  let msg = `你好，我需要采购 ${material.material_name}`;
  if (material.specification) msg += `，规格: ${material.specification}`;
  if (material.quantity && material.unit) msg += `，需求数量约 ${material.quantity}${material.unit}`;
  else if (material.quantity) msg += `，需求数量约 ${material.quantity}`;
  if (material.brand) msg += `，品牌要求: ${material.brand}`;
  msg += "。请问有现货吗？能提供报价吗？";
  return msg;
}

// ══════════════════════════════════════════════════════
//  工具函数（来自 run-1688.js）
// ══════════════════════════════════════════════════════

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

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

function sanitizeFileName(value) {
  return value.replace(/[\\/:*?"<>|]/g, "_").replace(/\s+/g, " ").trim().slice(0, 80);
}

function parseOfferId(url) {
  try {
    return new URL(url).searchParams.get("offerId") ?? "";
  } catch {
    return "";
  }
}

function createShortHash(value) {
  return crypto.createHash("sha1").update(value || "").digest("hex").slice(0, 12);
}

function createSafeStorageKey({ batchId, keyword, index, offerUrl, merchantUrl, imageUrl }) {
  const offerId = parseOfferId(offerUrl) || `offer-${createShortHash(offerUrl)}`;
  const merchantKey = merchantUrl ? `merchant-${createShortHash(merchantUrl)}` : "merchant-unknown";
  const keywordKey = `keyword-${createShortHash(keyword)}`;
  const extension = path.extname(new URL(normalizeUrl(imageUrl)).pathname) || ".jpg";
  return `${batchId}/${keywordKey}/${String(index).padStart(2, "0")}-${offerId}-${merchantKey}${extension.toLowerCase()}`;
}

function buildContactPageUrl(merchantUrl) {
  if (!merchantUrl) return "";

  try {
    const url = new URL(merchantUrl);
    url.pathname = "/page/contactinfo.htm";
    url.search = "";
    url.hash = "";
    return url.toString();
  } catch {
    return "";
  }
}

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

function normalizePhoneNumber(value) {
  const cleaned = cleanText(value).replace(/[：:]/g, " ");
  if (!cleaned || cleaned === "暂无") return cleaned;

  const digits = cleaned.replace(/\D/g, "");
  if (digits.length <= 3) return "";

  return cleaned;
}

function mergeMerchantContactInfo(extracted, merchantUrl) {
  const phone = normalizePhoneNumber(extracted.phone);
  const mobile = normalizePhoneNumber(extracted.mobile);

  return {
    contactPageUrl: extracted.contactPageUrl,
    merchantName: cleanText(extracted.merchantName),
    contactPerson: cleanText(extracted.contactPerson),
    phone,
    mobile,
    address: cleanText(extracted.address),
    error: extracted.error,
    merchantUrl,
  };
}

async function extractMerchantContactInfo(context, merchantUrl) {
  const contactPageUrl = buildContactPageUrl(merchantUrl);
  const emptyResult = {
    contactPageUrl,
    merchantName: "",
    contactPerson: "",
    phone: "",
    mobile: "",
    address: "",
    error: contactPageUrl ? "" : "missing-merchant-url",
  };

  if (!contactPageUrl) return emptyResult;

  const page = await context.newPage();

  try {
    await page.goto(contactPageUrl, { waitUntil: "domcontentloaded", timeout: 30_000 });
    await antiDetectPause();

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

    const extracted = await page.evaluate(() => {
      const clean = (value) => (value ?? "").replace(/\s+/g, " ").trim();

      const bodyText = clean(document.body?.innerText || "");
      const lines = bodyText
        .split(/\n+/)
        .map((line) => clean(line))
        .filter(Boolean);

      const titleSelectors = ["h1", ".company-name", "[class*='company']", "[class*='shop-name']", "[class*='store-name']"];

      let merchantName = "";
      for (const selector of titleSelectors) {
        const matches = Array.from(document.querySelectorAll(selector))
          .map((element) => clean(element.textContent))
          .filter(Boolean);
        const candidate = matches.find(
          (text) =>
            text !== "联系方式" &&
            text.length < 40 &&
            /公司|厂|商行|经营部|店|合作社/.test(text)
        );
        if (candidate) {
          merchantName = candidate;
          break;
        }
      }

      if (!merchantName) {
        merchantName =
          lines.find(
            (text) =>
              text.length < 40 && /公司|厂|商行|经营部|店|合作社/.test(text) && text !== "联系方式"
          ) || "";
      }

      const captureField = (label, nextLabels) => {
        const pattern = new RegExp(`${label}[：:]\\s*(.*?)\\s*(?=${nextLabels.join("|")}|$)`);
        const match = bodyText.match(pattern);
        return clean(match?.[1] || "");
      };

      const phone = captureField("电话", ["手机[：:]", "传真[：:]", "地址[：:]"]);
      const mobile = captureField("手机", ["传真[：:]", "地址[：:]"]);
      const address = captureField("地址", ["[\\u4e00-\\u9fa5]{2,8}(?:先生|女士|小姐)", "技术支持[：:]", "免责声明", "客服中心"]);

      const personMatches = bodyText.match(/[\u4e00-\u9fa5]{2,8}(?:先生|女士|小姐)/g) || [];
      const contactPerson = clean(personMatches.at(-1) || "");

      return {
        merchantName,
        contactPerson,
        phone,
        mobile,
        address,
      };
    });

    return mergeMerchantContactInfo({
      contactPageUrl,
      merchantName: extracted.merchantName,
      contactPerson: extracted.contactPerson,
      phone: extracted.phone,
      mobile: extracted.mobile,
      address: extracted.address,
      error: "",
    }, merchantUrl);
  } catch (error) {
    return {
      ...emptyResult,
      error: error instanceof Error ? error.message : String(error),
    };
  } finally {
    await page.close().catch(() => {});
  }
}

function isTransientSupabaseError(error) {
  const message = error?.message ?? "";
  const status = error?.status ?? 0;
  return status >= 500 || /502|503|504|bad gateway|gateway/i.test(message);
}

function isMissingSchemaColumnError(error) {
  const message = error?.message ?? "";
  return /Could not find the '.*' column of 'final_result_products' in the schema cache/i.test(message);
}

async function insertFinalResultProductWithRetry(supabase, payload, attempts = 3) {
  let lastError = null;

  for (let attempt = 1; attempt <= attempts; attempt += 1) {
    const response = await supabase.from("final_result_products").insert(payload).select("id").single();

    if (!response.error) {
      return response;
    }

    lastError = response.error;

    if (isMissingSchemaColumnError(response.error)) {
      throw new Error(
        `final_result_products 缺少必要字段，请先在 Supabase 执行最新 migration。原始错误: ${response.error.message}`
      );
    }

    if (!isTransientSupabaseError(response.error) || attempt === attempts) {
      return response;
    }

    await randomPause(1_000 * attempt, 1_800 * attempt);
  }

  return { data: null, error: lastError };
}

function isLoginUrl(url) {
  return url.includes("login.taobao.com") || url.includes("login.1688.com");
}

function looksLikeTitle(text) {
  return (
    text.length >= 8 &&
    !/^¥/.test(text) &&
    !text.includes("回头率") &&
    !text.includes("退货包运费") &&
    !text.includes("包邮") &&
    !text.includes("榜第") &&
    !text.includes("先采后付") &&
    !text.includes("满") &&
    !text.includes("件")
  );
}

// ══════════════════════════════════════════════════════
//  浏览器上下文
// ══════════════════════════════════════════════════════

async function createContext() {
  await ensureDir(PROFILE_DIR);
  console.log(`🔑 浏览器配置: ${PROFILE_DIR}`);
  const context = await chromium.launchPersistentContext(PROFILE_DIR, {
    channel: "chrome",
    headless: HEADLESS,
    locale: "zh-CN",
    viewport: { width: 1440, height: 960 },
    args: ["--disable-blink-features=AutomationControlled"],
  });
  return context;
}

// ══════════════════════════════════════════════════════
//  搜索相关（适配自 run-1688.js，参数化 keyword）
// ══════════════════════════════════════════════════════

async function promptForManualLogin(page, targetUrl) {
  const rl = readline.createInterface({ input, output });
  console.log("\n⚠️  1688/淘宝登录是前置条件，请在浏览器窗口完成登录，然后回到终端按 Enter 继续。");
  try {
    await rl.question("");
  } catch (error) {
    if (error && typeof error === "object" && "code" in error && error.code === "ABORT_ERR") {
      throw new Error("已取消等待手动登录");
    }
    throw error;
  } finally {
    rl.close();
  }
  await page.goto(targetUrl, { waitUntil: "domcontentloaded" });
  await antiDetectPause();
}

async function findVisibleSearchBox(page, timeoutMs = 12_000) {
  const startedAt = Date.now();
  while (Date.now() - startedAt < timeoutMs) {
    const locator = page.locator('input[type="text"], input:not([type])');
    const total = await locator.count();
    for (let index = 0; index < total; index += 1) {
      const candidate = locator.nth(index);
      if (await candidate.isVisible().catch(() => false)) return candidate;
    }
    await randomPause(200, 400);
  }
  return null;
}

async function waitForSearchResultsPage(context, fallbackPage) {
  for (let attempt = 0; attempt < 40; attempt += 1) {
    const target =
      context.pages().find((p) => p.url().includes("/offer_search.htm")) ??
      (fallbackPage.url().includes("/offer_search.htm") ? fallbackPage : null);
    if (target) {
      await target.bringToFront().catch(() => {});
      return target;
    }
    await randomPause(250, 450);
  }
  throw new Error("未检测到 1688 搜索结果页");
}

async function attemptHomeSearch(context, page, keyword) {
  const searchBox = await findVisibleSearchBox(page);
  if (!searchBox) return { resultsPage: null, loginPage: null };

  await humanType(page, searchBox, keyword);
  await randomPause(200, 500);
  await page.keyboard.press("Enter");
  await antiDetectPause();

  const loginPage = context.pages().find((p) => isLoginUrl(p.url())) ?? null;
  if (loginPage) return { resultsPage: null, loginPage };

  const resultsPage = await waitForSearchResultsPage(context, page).catch(() => null);
  return { resultsPage, loginPage: null };
}

async function openSearchResults(context, keyword, retryAfterLogin = true) {
  const page = context.pages()[0] ?? (await context.newPage());
  await page.goto("https://www.1688.com/", { waitUntil: "domcontentloaded" });
  await page.setViewportSize({ width: 1440, height: 960 });
  await antiDetectPause();

  const firstAttempt = await attemptHomeSearch(context, page, keyword);
  if (firstAttempt.resultsPage) {
    await firstAttempt.resultsPage.waitForLoadState("domcontentloaded");
    await antiDetectPause();
    return firstAttempt.resultsPage;
  }

  if (firstAttempt.loginPage && retryAfterLogin) {
    await promptForManualLogin(firstAttempt.loginPage, "https://www.1688.com/");
    return openSearchResults(context, keyword, false);
  }

  // 降级：直接 URL 导航
  const fallbackPage = context.pages()[0] ?? page;
  const fallbackUrl = build1688OfferSearchUrl(keyword);
  await fallbackPage.goto(fallbackUrl, { waitUntil: "domcontentloaded" });

  if (isLoginUrl(fallbackPage.url()) && retryAfterLogin) {
    await promptForManualLogin(fallbackPage, fallbackUrl);
    await fallbackPage.goto(fallbackUrl, { waitUntil: "domcontentloaded" });
  }

  await fallbackPage.waitForLoadState("domcontentloaded");
  await antiDetectPause();
  return fallbackPage;
}

// ── 提取搜索结果（来自 run-1688.js，核心 DOM 解析逻辑）────
async function extractTopResults(page, limit) {
  await page.waitForSelector('a[href*="offerId="]', { timeout: 30_000 });
  await humanScrollPage(page, 3);

  const rawResults = await page.evaluate((take) => {
    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 isMerchantLink(link) {
      return (
        link.href.includes(".1688.com/") &&
        !link.href.includes("offerId=") &&
        !link.href.includes("def_cbu_web_im") &&
        !link.href.includes("similar_search") &&
        !link.href.includes("fcaReport") &&
        !link.href.includes("auth/")
      );
    }

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

    function looksLikeDescendantTitle(value) {
      return (
        value.length >= 10 &&
        !value.startsWith("¥") &&
        !value.includes("采购咨询") &&
        !value.includes("退换体验") &&
        !value.includes("品质体验") &&
        !value.includes("纠纷解决") &&
        !value.includes("物流时效") &&
        !value.includes("回头率") &&
        !value.includes("退货包运费") &&
        !value.includes("先采后付") &&
        !value.includes("官方物流") &&
        !value.includes("验厂报告") &&
        value !== "找相似" &&
        value !== "旺旺在线"
      );
    }

    function pickTitle(link, merchantName) {
      const textBlocks = Array.from(link.querySelectorAll("*"))
        .map((node) => clean(node.textContent))
        .filter(Boolean);
      const nestedTitle = textBlocks.find(looksLikeDescendantTitle);
      if (nestedTitle) return nestedTitle;

      let text = clean(link.textContent)
        .replace(/^找相似/, "")
        .replace(/旺旺在线$/, "");
      if (merchantName) text = text.replace(merchantName, "");

      const metrics = [
        /综合服务验厂报告/g, /综合服务/g, /验厂报告/g,
        /采购咨询\d+(\.\d+)?/g, /退换体验\d+(\.\d+)?/g,
        /品质体验\d+(\.\d+)?/g, /纠纷解决\d+(\.\d+)?/g,
        /物流时效\d+(\.\d+)?/g, /回头率\d+%/g,
        /退货包运费/g, /先采后付/g, /官方物流/g, /旺旺在线/g,
        /¥\d+(\.\d+)?/g, /\d+\+?件/g, /满\d+[^\s]*/g,
        /首单减\d+[^\s]*/g, /最高返\d+[^\s]*/g, /商机组货：[^\s]+/g,
      ];
      for (const pattern of metrics) text = text.replace(pattern, " ");
      return clean(text);
    }

    for (let index = 0; index < anchors.length; index += 1) {
      const offerLink = anchors[index];
      if (!isOfferLink(offerLink)) continue;
      const offerUrl = offerLink.href;
      if (!offerUrl || seen.has(offerUrl)) continue;
      seen.add(offerUrl);

      let merchantLink = null;
      let contactLink = null;
      for (let cursor = index + 1; cursor < anchors.length; cursor += 1) {
        const candidate = anchors[cursor];
        if (isOfferLink(candidate)) break;
        if (!merchantLink && isMerchantLink(candidate)) { merchantLink = candidate; continue; }
        if (!contactLink && candidate.href.includes("def_cbu_web_im")) contactLink = candidate;
      }

      const image = offerLink.querySelector("img");
      const merchantName = clean(merchantLink?.textContent);

      items.push({
        title: pickTitle(offerLink, merchantName),
        offerUrl,
        imageUrl: image?.getAttribute("src") ?? image?.getAttribute("data-src") ?? image?.getAttribute("data-lazy-src") ?? "",
        merchantName,
        merchantUrl: merchantLink?.href ?? "",
        contactUrl: contactLink?.href ?? "",
      });
      if (items.length >= take) break;
    }
    return items;
  }, limit);

  return rawResults
    .map((item, index) =>
      normalizeOfferRecord({ ...item, imageUrl: normalizeUrl(item.imageUrl) }, index + 1)
    )
    .map((item) => ({
      ...item,
      title: looksLikeTitle(item.title) ? item.title : item.title || `1688结果-${item.index}`,
    }));
}

// ══════════════════════════════════════════════════════
//  图片上传到 Supabase Storage
// ══════════════════════════════════════════════════════

async function uploadImageToStorage(supabase, { imageUrl, index, keyword, batchId, offerUrl, merchantUrl }) {
  if (!imageUrl) return { storagePath: "", publicUrl: "", error: "无图片 URL" };

  try {
    const response = await fetch(normalizeUrl(imageUrl), {
      headers: {
        "user-agent":
          "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
      },
    });
    if (!response.ok) return { storagePath: "", publicUrl: "", error: `HTTP ${response.status}` };

    const buffer = Buffer.from(await response.arrayBuffer());
    const storagePath = createSafeStorageKey({
      batchId,
      keyword,
      index,
      offerUrl,
      merchantUrl,
      imageUrl,
    });
    const ext = path.extname(storagePath) || ".jpg";

    const { error: uploadError } = await supabase.storage
      .from(STORAGE_BUCKET)
      .upload(storagePath, buffer, { contentType: `image/${ext.slice(1)}`, upsert: true });

    if (uploadError) return { storagePath, publicUrl: "", error: uploadError.message };

    const { data } = supabase.storage.from(STORAGE_BUCKET).getPublicUrl(storagePath);
    return { storagePath, publicUrl: data.publicUrl, error: "" };
  } catch (err) {
    return { storagePath: "", publicUrl: "", error: err instanceof Error ? err.message : String(err) };
  }
}

// ══════════════════════════════════════════════════════
//  数据库写入
// ══════════════════════════════════════════════════════

function buildDetectedContacts(result, merchantContact) {
  const contacts = [];

  if (merchantContact.phone) {
    contacts.push({
      type: "phone",
      value: merchantContact.phone,
      source: "merchant_contact_page",
    });
  }

  if (merchantContact.mobile && merchantContact.mobile !== merchantContact.phone) {
    contacts.push({
      type: "mobile",
      value: merchantContact.mobile,
      source: "merchant_contact_page",
    });
  }

  if (result.contact?.url) {
    contacts.push({
      type: result.contact.type || "wangwang",
      value: result.contact.url,
      source: "search_result_contact",
      context: result.contact.handle || result.contact.site || "1688",
    });
  }

  return contacts;
}

async function clearMaterialSearchArtifacts(supabase, { batchId, materialId }) {
  const { data: existingRows, error: existingError } = await supabase
    .from("final_result_products")
    .select("id")
    .eq("batch_id", batchId)
    .eq("material_id", materialId);

  if (existingError) {
    throw new Error(`读取 final_result_products 失败: ${existingError.message}`);
  }

  const existingIds = (existingRows ?? []).map((row) => row.id).filter(Boolean);
  if (existingIds.length > 0) {
    const [contactLogsRes, customerChatsRes] = await Promise.all([
      supabase.from("contact_logs").delete().in("search_result_id", existingIds),
      supabase.from("customer_service_chats").delete().in("search_result_id", existingIds),
    ]);

    if (contactLogsRes.error) {
      throw new Error(`删除旧 contact_logs 失败: ${contactLogsRes.error.message}`);
    }

    if (customerChatsRes.error) {
      throw new Error(`删除旧 customer_service_chats 失败: ${customerChatsRes.error.message}`);
    }
  }

  const { error: deleteError } = await supabase
    .from("final_result_products")
    .delete()
    .eq("batch_id", batchId)
    .eq("material_id", materialId);

  if (deleteError) {
    throw new Error(`删除旧 final_result_products 失败: ${deleteError.message}`);
  }
}

async function saveSearchResult(supabase, { batchId, keyword, materialId, result, image, merchantContact }) {
  const payload = {
    batch_id: batchId,
    material_id: materialId,
    source: "legacy-run-supabase",
    main_keyword: keyword,
    finished_at: new Date().toISOString(),
    rank: result.index,
    title: result.title,
    offer_url: result.offerUrl,
    image_url: image.publicUrl || result.imageUrl,
    merchant_name: merchantContact.merchantName || result.merchantName,
    merchant_url: result.merchantUrl,
    wangwang_url: result.contact.url,
    offer_screenshot_url: image.publicUrl,
    contact_page_url: merchantContact.contactPageUrl,
    detected_contacts: buildDetectedContacts(result, merchantContact),
  };

  const { data, error } = await insertFinalResultProductWithRetry(supabase, payload);

  if (error) {
    console.error(`  ❌ 搜索结果入库失败: ${error.message}`);
    return null;
  }
  console.log(`  💾 搜索结果已写入 final_result_products: ${result.title.slice(0, 40)}`);
  return data?.id ?? null;
}

async function saveMerchantContact(supabase, { batchId, result, merchantContact }) {
  if (!result.merchantUrl) {
    return;
  }

  const payload = {
    merchant_url: result.merchantUrl,
    merchant_name: merchantContact.merchantName || result.merchantName,
    contact_page_url: merchantContact.contactPageUrl,
    contact_person: merchantContact.contactPerson,
    phone: merchantContact.phone,
    mobile: merchantContact.mobile,
    address: merchantContact.address,
    source_batch_id: batchId,
    source_offer_url: result.offerUrl,
    updated_at: new Date().toISOString(),
  };

  const { error } = await supabase
    .from("merchant_contacts")
    .upsert(payload, { onConflict: "merchant_url" });

  if (error) {
    if (/Could not find the table 'public\.merchant_contacts' in the schema cache/i.test(error.message)) {
      console.warn("  ⚠️  merchant_contacts 表不存在，请先执行迁移 005_merchant_contacts.sql");
      return;
    }

    console.error(`  ❌ 商家联系方式入库失败: ${error.message}`);
    return;
  }

  console.log(`  💾 商家联系方式已沉淀: ${(merchantContact.merchantName || result.merchantName || result.merchantUrl).slice(0, 40)}`);
}

async function saveContactLog(supabase, { batchId, searchResultId, keyword, result, contactStatus, messageSent }) {
  const { error } = await supabase.from("contact_logs").insert({
    batch_id: batchId,
    search_result_id: searchResultId ?? null,
    keyword,
    merchant_name: result.merchantName,
    title: result.title,
    offer_url: result.offerUrl,
    contact_url: result.contact.url,
    message_sent: messageSent,
    status: contactStatus.status,
    reason: contactStatus.reason ?? "",
    final_url: contactStatus.finalUrl ?? "",
    conversation_code: contactStatus.conversationCode ?? "",
  });

  if (error) console.error(`  ❌ 联系记录入库失败: ${error.message}`);
  else console.log(`  💾 联系记录已入库: [${contactStatus.status}] ${result.merchantName}`);
}

// ══════════════════════════════════════════════════════
//  旺旺聊天（来自 run-1688.js）
// ══════════════════════════════════════════════════════

async function clickIfVisible(locator, { force = false } = {}) {
  if ((await locator.count()) === 0) return false;
  const target = locator.first();
  if (!(await target.isVisible().catch(() => false))) return false;
  await target.click({ force }).catch(async () => {
    await target.evaluate((el) => el.click());
  });
  return true;
}

async function chooseWebChatIfNeeded(page) {
  const webChat = page.getByText("1688网页版聊天").first();
  if (await clickIfVisible(webChat)) await antiDetectPause();
  const confirm = page.getByText("确定").first();
  if (await clickIfVisible(confirm)) await antiDetectPause();
}

async function skipContactInfoIfNeeded(page) {
  if (!page.url().includes("modify_evolve.htm")) return false;
  const later = page.getByText("下次再说").first();
  if (await clickIfVisible(later)) { await antiDetectPause(); return true; }
  const never = page.getByText("不再提醒").first();
  if (await clickIfVisible(never)) { await antiDetectPause(); return true; }
  return false;
}

async function getChatFrame(page) {
  for (let attempt = 0; attempt < 25; attempt += 1) {
    const frame = page.frames().find((f) => f.url().includes("def_cbu_web_im_core"));
    if (frame) return frame;
    await randomPause(300, 600);
  }
  return null;
}

async function dismissChatGuide(frame) {
  const know = frame.getByText("我知道了").first();
  if (await clickIfVisible(know, { force: true })) await antiDetectPause();
}

async function findChatInput(scope) {
  const candidates = [
    scope.locator("textarea").first(),
    scope.locator('[contenteditable="true"]').last(),
    scope.getByPlaceholder(/请输入|发送|输入/i).first(),
    scope.locator('input[type="text"]').last(),
  ];
  for (const locator of candidates) {
    if ((await locator.count()) > 0 && (await locator.first().isVisible().catch(() => false))) {
      return locator.first();
    }
  }
  return null;
}

async function findSendButton(scope) {
  const candidates = [
    scope.getByRole("button", { name: /发送/ }).first(),
    scope.getByText(/^发送$/).first(),
    scope.locator('[data-role="send"]').first(),
  ];
  for (const locator of candidates) {
    if ((await locator.count()) > 0 && (await locator.first().isVisible().catch(() => false))) {
      return locator.first();
    }
  }
  return null;
}

function buildConversationNick(result) {
  if (!result.contact?.handle) return "";
  if (result.contact.site) return `${result.contact.site}${result.contact.handle}`;
  return result.contact.handle;
}

async function createAndSelectConversation(frame, result) {
  const conversationNick = buildConversationNick(result);
  if (!conversationNick) return { ok: false, reason: "missing-contact-handle" };

  const created = await frame.evaluate(
    async ({ handle, conversationNick }) => {
      const sdk = window._imsdk;
      const conversationService = sdk?.getConversationService?.();
      if (!conversationService) return { ok: false, reason: "imsdk-conversation-service-missing" };

      const identifier = {
        entityType: "U",
        bizType: "11152",
        target: { targetId: handle, targetType: "8" },
        createContext: {
          createConversationCtx: JSON.stringify({ othersNick: [conversationNick] }),
        },
      };

      const createdConversation = await new Promise((resolve) => {
        conversationService.createSingleConversation({
          identifier,
          dataCallback: (data) => resolve({ ok: true, data }),
          completeCallback: () => {},
          errorCallBack: (error) =>
            resolve({ ok: false, reason: error?.message ?? error?.msg ?? JSON.stringify(error) }),
        });
      });

      if (!createdConversation.ok) return createdConversation;

      const conversationCode = createdConversation.data?.conversationCode;
      if (!conversationCode) return { ok: false, reason: "conversation-code-missing" };

      await new Promise((resolve) => {
        conversationService.enterConversationByCCode({
          ccode: conversationCode,
          dataCallback: () => resolve(null),
          completeCallback: () => resolve(null),
          errorCallBack: () => resolve(null),
        });
      });

      return {
        ok: true,
        conversationCode,
        conversationName:
          createdConversation.data?.conversationIdentifier?.conversationName ??
          createdConversation.data?.conversationName ??
          handle,
      };
    },
    { handle: result.contact.handle, conversationNick }
  );

  if (!created.ok) return created;

  await dismissChatGuide(frame);
  await antiDetectPause();

  const candidateTexts = [created.conversationName, result.contact.handle, result.merchantName].filter(Boolean);
  for (const text of candidateTexts) {
    const item = frame.getByText(text, { exact: false }).first();
    if (await clickIfVisible(item, { force: true })) {
      await antiDetectPause();
      return { ok: true, conversationCode: created.conversationCode, selectedText: text };
    }
  }
  return { ok: false, reason: "conversation-list-item-not-found", conversationCode: created.conversationCode };
}

async function typeIntoChat(frame, message) {
  const editor = frame.locator('[contenteditable="true"]').last();
  if ((await editor.count()) > 0 && (await editor.isVisible().catch(() => false))) {
    await editor.evaluate((el, value) => {
      el.textContent = value;
      el.dispatchEvent(new InputEvent("input", { bubbles: true }));
    }, message);
    return true;
  }
  const chatInput = await findChatInput(frame);
  if (!chatInput) return false;
  await chatInput.fill(message).catch(async () => {
    await humanType(frame.page(), chatInput, message, { clear: false });
  });
  return true;
}

async function contactMerchant(context, result, askMessage) {
  if (!result.contact.url) {
    return { status: "skipped", reason: "missing-contact-url" };
  }

  const page = await context.newPage();
  try {
    await page.goto(result.contact.url, { waitUntil: "domcontentloaded" });
    await antiDetectPause();

    if (isLoginUrl(page.url())) {
      await promptForManualLogin(page, result.contact.url);
    }

    await chooseWebChatIfNeeded(page);
    await skipContactInfoIfNeeded(page);
    const frame = await getChatFrame(page);

    if (!frame) {
      return { status: "skipped", reason: "chat-frame-not-found", finalUrl: page.url() };
    }

    await dismissChatGuide(frame);
    const frameTextBefore = await frame.locator("body").innerText().catch(() => "");
    let conversationStatus = { ok: true };

    if (frameTextBefore.includes("您尚未选择联系人")) {
      conversationStatus = await createAndSelectConversation(frame, result);
      if (!conversationStatus.ok) {
        return {
          status: "skipped",
          reason: conversationStatus.reason ?? "contact-selection-required",
          finalUrl: page.url(),
        };
      }
    }

    const typed = await typeIntoChat(frame, askMessage);
    if (!typed) {
      return { status: "skipped", reason: "chat-input-not-found", finalUrl: page.url() };
    }

    await antiDetectPause();
    const sendButton = await findSendButton(frame);
    if (sendButton) {
      await sendButton.click({ force: true }).catch(async () => {
        await humanClick(page, sendButton);
      });
    } else {
      await page.keyboard.press("Enter");
    }

    await antiDetectPause();
    const frameTextAfter = await frame.locator("body").innerText().catch(() => "");
    const confirmed = frameTextAfter.includes(askMessage.slice(0, 10));

    return {
      status: confirmed ? "sent" : "unknown",
      reason: confirmed ? "" : "message-not-confirmed-in-frame",
      finalUrl: page.url(),
      conversationCode: conversationStatus.conversationCode ?? "",
    };
  } catch (error) {
    return {
      status: "error",
      reason: error instanceof Error ? error.message : String(error),
      finalUrl: page.url(),
    };
  } finally {
    await page.close().catch(() => {});
  }
}

// ══════════════════════════════════════════════════════
//  主流程
// ══════════════════════════════════════════════════════

async function main() {
  console.log("\n╔══════════════════════════════════════════════════════╗");
  console.log("║   1688 采购自动化 (Supabase 集成版)                  ║");
  console.log("╚══════════════════════════════════════════════════════╝\n");

  // 1. 初始化 Supabase
  const supabase = initSupabase();
  console.log("✅ Supabase 客户端已初始化");

  // 2. 从 Supabase 读取原始资料
  const materials = await fetchMaterials(supabase);
  console.log(`📦 从 procurement_materials 读取到 ${materials.length} 个关键词:`);
  for (const m of materials) {
    console.log(`   - ${m.material_name}${m.specification ? ` (${m.specification})` : ""}`);
  }

  // 3. 启动浏览器
  console.log("\n🚀 正在启动浏览器...");
  const context = await createContext();
  const verificationMonitor = attachVerificationMonitor(context, { logPrefix: "[run-supabase][验证码]" });

  // 清理多余标签页
  while (context.pages().length > 1) {
    await context.pages()[context.pages().length - 1].close().catch(() => {});
  }

  try {
    // 4. 串行处理每个物料
    for (let i = 0; i < materials.length; i++) {
      const material = materials[i];
      const keyword = material.material_name;
      const batchId = cleanText(material.batch_id);
      const askMessage = composeAskMessage(material);

      if (!batchId) {
        console.error(`\n❌ 物料 ${material.id} 缺少 batch_id，跳过`);
        continue;
      }

      console.log(`\n${"═".repeat(56)}`);
      console.log(`📌 [${i + 1}/${materials.length}] 关键词: "${keyword}"`);
      console.log(`🏷️  对应批次: ${batchId}`);
      console.log(`💬 询价消息: "${askMessage.slice(0, 60)}${askMessage.length > 60 ? "..." : ""}"`);
      console.log(`${"─".repeat(56)}`);

      // 5a. 搜索
      let resultsPage;
      try {
        resultsPage = await openSearchResults(context, keyword);
      } catch (err) {
        console.error(`  ❌ 搜索失败: ${err instanceof Error ? err.message : String(err)}`);
        continue;
      }

      let topResults;
      try {
        topResults = await extractTopResults(resultsPage, RESULT_LIMIT);
      } catch (err) {
        console.error(`  ❌ 结果提取失败: ${err instanceof Error ? err.message : String(err)}`);
        continue;
      }
      console.log(`  📋 提取到 ${topResults.length} 个搜索结果`);

      if (topResults.length === 0) {
        console.log("  ⚠️  无搜索结果，跳过此关键词");
        continue;
      }

      await clearMaterialSearchArtifacts(supabase, {
        batchId,
        materialId: material.id,
      });

      // 5b. 下载图片 → 上传 Storage → 写入 final_result_products
      const enrichedResults = [];
      for (const result of topResults) {
        console.log(`\n  🖼️  [${result.index}] ${result.title.slice(0, 50)}`);
        const merchantContact = await extractMerchantContactInfo(context, result.merchantUrl);
        if (merchantContact.contactPageUrl) {
          console.log(`     📇 联系页: ${merchantContact.contactPageUrl}`);
          if (merchantContact.phone || merchantContact.mobile || merchantContact.address) {
            console.log(`     ✅ 已抓取联系方式`);
          } else if (merchantContact.error) {
            console.log(`     ⚠️  联系方式抓取失败: ${merchantContact.error}`);
          } else {
            console.log("     ⚠️  联系方式页存在，但未提取到电话/地址");
          }
        }

        const image = await uploadImageToStorage(supabase, {
          imageUrl: result.imageUrl,
          index: result.index,
          keyword,
          batchId,
          offerUrl: result.offerUrl,
          merchantUrl: result.merchantUrl,
        });
        if (image.error) console.log(`     ⚠️  图片: ${image.error}`);
        else console.log(`     ✅ 图片已上传`);

        const searchResultId = await saveSearchResult(supabase, {
          batchId,
          keyword,
          materialId: material.id,
          result,
          image,
          merchantContact,
        });

        await saveMerchantContact(supabase, {
          batchId,
          result,
          merchantContact,
        });

        enrichedResults.push({ ...result, searchResultId, merchantContact });
        await randomPause(300, 800);
      }

      // 5c. 联系商家
      if (CONTACT_MERCHANTS) {
        console.log(`\n  📞 开始联系商家...`);
        const contacted = new Set();

        for (const result of enrichedResults) {
          const dedupeKey = result.contact.url || result.merchantUrl || result.offerUrl;
          if (contacted.has(dedupeKey)) {
            await saveContactLog(supabase, {
              batchId,
              searchResultId: result.searchResultId,
              keyword,
              result,
              contactStatus: { status: "skipped", reason: "duplicate-merchant" },
              messageSent: askMessage,
            });
            continue;
          }
          contacted.add(dedupeKey);

          console.log(`\n  🗨️  联系: ${result.merchantName || result.contact.handle || "未知商家"}`);
          const contactStatus = await contactMerchant(context, result, askMessage);
          console.log(`     结果: [${contactStatus.status}] ${contactStatus.reason || "OK"}`);

          await saveContactLog(supabase, {
            batchId,
            searchResultId: result.searchResultId,
            keyword,
            result,
            contactStatus,
            messageSent: askMessage,
          });
          await antiDetectPause(3_000, 6_000);
        }
      }

      // 5d. 关闭多余标签页
      for (const p of context.pages().slice(1)) {
        await p.close().catch(() => {});
      }

      // 5e. 关键词间等待
      if (i < materials.length - 1) {
        const wait = randomBetween(8_000, 15_000);
        console.log(`\n⏳ 关键词间隔等待 ${(wait / 1000).toFixed(1)}s...`);
        await new Promise((r) => setTimeout(r, wait));
      }
    }

    console.log(`\n${"═".repeat(56)}`);
    console.log(`✅ 全部完成！批次 ID: ${batchId}`);
    console.log(`   共处理 ${materials.length} 个关键词`);
    console.log(`${"═".repeat(56)}\n`);
  } finally {
    verificationMonitor.dispose();
    if (!KEEP_BROWSER_OPEN) {
      await context.close();
    }
  }
}

main().catch((error) => {
  console.error(`\n💥 程序崩溃: ${error instanceof Error ? error.message : String(error)}`);
  if (error instanceof Error && error.stack) console.error(error.stack);
  process.exitCode = 1;
});
