import { mkdir, 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 { chromium } from "playwright";

import { attachVerificationMonitor, humanClick, humanScrollPage, humanType, randomPause, antiDetectPause } from "./human.js";
import { normalizeOfferRecord } from "./offer-normalizer.js";

const SEARCH_QUERY = process.env.SEARCH_QUERY ?? "苹果新机 17";
const ASK_MESSAGE = process.env.ASK_MESSAGE ?? "是否有 iPhone 17 Pro Max 256G";
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 ROOT_DIR = process.cwd();
const OUTPUT_DIR = path.join(ROOT_DIR, "output");
const IMAGE_DIR = path.join(OUTPUT_DIR, "images");
const PROFILE_DIR = path.join(ROOT_DIR, ".browser", "1688-profile");

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 {
    const parsed = new URL(url);
    return parsed.searchParams.get("offerId") ?? "";
  } catch {
    return "";
  }
}

function isLikelyMerchantLink(url) {
  if (!url) {
    return false;
  }

  return (
    url.includes(".1688.com") &&
    !url.includes("offerId=") &&
    !url.includes("def_cbu_web_im") &&
    !url.includes("login.")
  );
}

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 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}`
    }));
}

async function saveImage(imageUrl, index, title) {
  if (!imageUrl) {
    return "";
  }

  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) {
    throw new Error(`下载图片失败: ${response.status}`);
  }

  const buffer = Buffer.from(await response.arrayBuffer());
  const extension = path.extname(new URL(normalizeUrl(imageUrl)).pathname) || ".jpg";
  const fileName = `${String(index).padStart(2, "0")}-${sanitizeFileName(title) || "image"}${extension}`;
  const imagePath = path.join(IMAGE_DIR, fileName);
  await writeFile(imagePath, buffer);
  return imagePath;
}

async function saveResults(results) {
  const enriched = [];

  for (const result of results) {
    let savedImagePath = "";
    let imageError = "";

    try {
      savedImagePath = await saveImage(result.imageUrl, result.index, result.title);
    } catch (error) {
      imageError = error instanceof Error ? error.message : String(error);
    }

    enriched.push({
      ...result,
      savedImagePath,
      imageError
    });
  }

  const outputPath = path.join(OUTPUT_DIR, "results.json");
  await writeFile(outputPath, JSON.stringify(enriched, null, 2), "utf8");
  return { outputPath, results: enriched };
}

async function waitForSearchResultsPage(context, fallbackPage) {
  for (let attempt = 0; attempt < 40; attempt += 1) {
    const target =
      context.pages().find((page) => page.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 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 attemptHomeSearch(context, page) {
  const searchBox = await findVisibleSearchBox(page);

  if (!searchBox) {
    return { resultsPage: null, loginPage: null };
  }

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

  const loginPage = context.pages().find((candidate) => isLoginUrl(candidate.url())) ?? null;

  if (loginPage) {
    return { resultsPage: null, loginPage };
  }

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

async function openSearchResults(context, 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);

  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, false);
  }

  const fallbackPage = context.pages()[0] ?? page;
  const fallbackUrl = `https://s.1688.com/selloffer/offer_search.htm?charset=utf8&keywords=${encodeURIComponent(SEARCH_QUERY)}`;
  await fallbackPage.goto(fallbackUrl, { waitUntil: "domcontentloaded" });

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

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

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

async function promptForManualLogin(page, contactUrl) {
  const rl = readline.createInterface({ input, output });
  console.log("\n1688/淘宝登录是发消息前置条件。请在已打开的浏览器窗口完成登录，然后回到终端按 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(contactUrl, { waitUntil: "domcontentloaded" });
  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;
}

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((element) => element.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((candidate) => candidate.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();
  }
}

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((element, value) => {
      element.textContent = value;
      element.dispatchEvent(new InputEvent("input", { bubbles: true }));
    }, message);
    return true;
  }

  const input = await findChatInput(frame);
  if (!input) {
    return false;
  }

  await input.fill(message).catch(async () => {
    await humanType(frame.page(), input, message, { clear: false });
  });
  return true;
}

async function contactMerchant(context, result) {
  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, ASK_MESSAGE);
    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(ASK_MESSAGE);

    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() {
  await ensureDir(OUTPUT_DIR);
  await ensureDir(IMAGE_DIR);
  await ensureDir(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"]
  });
  const verificationMonitor = attachVerificationMonitor(context, { logPrefix: "[run-1688][验证码]" });

  try {
    const resultsPage = await openSearchResults(context);
    const topResults = await extractTopResults(resultsPage, RESULT_LIMIT);
    const { outputPath, results } = await saveResults(topResults);

    const contacted = new Set();
    const contactLogs = [];

    if (CONTACT_MERCHANTS) {
      for (const result of results) {
        const dedupeKey = result.contact.url || result.merchantUrl || result.offerUrl;

        if (contacted.has(dedupeKey)) {
          contactLogs.push({
            index: result.index,
            merchantName: result.merchantName,
            status: "skipped",
            reason: "duplicate-merchant"
          });
          continue;
        }

        contacted.add(dedupeKey);
        const status = await contactMerchant(context, result);
        contactLogs.push({
          index: result.index,
          merchantName: result.merchantName,
          title: result.title,
          ...status
        });
        await antiDetectPause(3_000, 6_000);
      }
    } else {
      for (const result of results) {
        contactLogs.push({
          index: result.index,
          merchantName: result.merchantName,
          title: result.title,
          status: "skipped",
          reason: "CONTACT_MERCHANTS=0"
        });
      }
    }

    const contactLogPath = path.join(OUTPUT_DIR, "contact-log.json");
    await writeFile(contactLogPath, JSON.stringify(contactLogs, null, 2), "utf8");

    console.log(`已保存搜索结果: ${outputPath}`);
    console.log(`已保存联系记录: ${contactLogPath}`);
  } finally {
    verificationMonitor.dispose();
    if (!KEEP_BROWSER_OPEN) {
      await context.close();
    }
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
