/**
 * wangwang-rpa.js
 *
 * 旺旺 IM RPA 公共函数。
 * 从 run-1688.js / run-supabase.js 中提取公共逻辑，并新增消息抓取能力。
 */

import crypto from "node:crypto";
import { existsSync } from "node:fs";
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { resolveChatTaskContactIdentity } from "./contact-identity.js";
import { createResolvedSupabaseClient } from "./supabase-client.js";
import { humanClick, humanType, randomPause, antiDetectPause } from "./human.js";

const WANGWANG_IMAGE_UPLOAD_MAX_BYTES = 2_900_000;

// ══════════════════════════════════════════════════════
//  登录检测 & 手动登录辅助
// ══════════════════════════════════════════════════════

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

/**
 * 弹出终端提示，让用户手动完成登录后按 Enter 继续。
 * 仅在非 headless 模式下有效。
 */
export async function promptForManualLogin(page, targetUrl, accountName = null) {
  const { createInterface } = await import("node:readline/promises");
  const { stdin, stdout } = await import("node:process");
  const rl = createInterface({ input: stdin, output: stdout });
  console.log("\n⚠️  1688/淘宝登录是前置条件，请在浏览器窗口完成登录，然后回到终端按 Enter 继续。");
  try {
    await rl.question("");
  } catch (error) {
    if (error && typeof error === "object" && "code" in error && error.code === "ABORT_ERR") {
      if (accountName) {
        await reportLoginStatusInline(accountName, "failed", "已取消等待手动登录");
      }
      throw new Error("已取消等待手动登录");
    }
    if (accountName) {
      await reportLoginStatusInline(accountName, "failed", error.message || String(error));
    }
    throw error;
  } finally {
    rl.close();
  }
  await page.goto(targetUrl, { waitUntil: "domcontentloaded" });
  await antiDetectPause();
}

/** Report login status to Supabase procurement_accounts_1688 table */
async function reportLoginStatusInline(accountName, status, error = null) {
  try {
    if (!accountName) return;
    const supabase = createResolvedSupabaseClient(process.env);
    await supabase
      .from("procurement_accounts_1688")
      .update({ login_status: status, login_error: error, login_checked_at: new Date().toISOString() })
      .eq("account_name", accountName);
  } catch (err) {
    console.warn("[wangwang-rpa] 写入登录状态失败:", err.message);
  }
}

// ══════════════════════════════════════════════════════
//  旺旺聊天 iframe 操作
// ══════════════════════════════════════════════════════

/**
 * 等待并获取旺旺聊天 iframe
 */
export async function getChatFrame(page, maxAttempts = 60) {
  for (let attempt = 0; attempt < maxAttempts; 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;
}

export async function waitForChatUiReady(frame) {
  await frame.waitForFunction(() => {
    const bodyText = document.body?.innerText || "";
    const conversationService = window._imsdk?.getConversationService?.();
    return Boolean(conversationService) && (
      bodyText.includes("请输入消息")
      || bodyText.includes("您尚未选择联系人")
      || document.querySelector('[contenteditable="true"], textarea, input[type="file"]')
    );
  }, null, { timeout: 30_000 }).catch(() => {});
}

/**
 * 关闭聊天新手引导
 */
export async function dismissChatGuide(frame) {
  const know = frame.getByText("我知道了").first();
  if (await clickIfVisible(know, { force: true })) {
    await antiDetectPause();
  }
}

/**
 * 选择"1688网页版聊天"并确认
 */
export 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();
}

/**
 * 跳过联系方式完善弹窗
 */
export 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;
}

// ══════════════════════════════════════════════════════
//  聊天输入 & 发送
// ══════════════════════════════════════════════════════

export 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;
}

export 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;
}

export 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;
}

/**
 * 在聊天框中输入消息
 */
export 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;
}

/**
 * 发送消息（输入 + 点发送按钮 / 按 Enter）
 */
export async function sendChatMessage(frame, page, message) {
  const typed = await typeIntoChat(frame, message);
  if (!typed) return { ok: false, reason: "chat-input-not-found" };

  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();
  return { ok: true };
}

async function resolveImageFile(imageConfig) {
  const source = String(imageConfig?.storagePath || imageConfig?.publicUrl || imageConfig?.dataUrl || imageConfig?.url || "").trim();
  if (!source) {
    return { ok: false, reason: "image-source-missing" };
  }

  if (/^data:image\/[^;]+;base64,/i.test(source)) {
    const matched = source.match(/^data:(image\/[^;]+);base64,(.+)$/i);
    if (!matched) {
      return { ok: false, reason: "image-data-url-invalid" };
    }

    const mimeType = matched[1].toLowerCase();
    const extension = mimeType.includes("png")
      ? ".png"
      : mimeType.includes("webp")
        ? ".webp"
        : mimeType.includes("gif")
          ? ".gif"
          : ".jpg";
    const dir = await mkdtemp(path.join(os.tmpdir(), "wangwang-image-"));
    const filePath = path.join(dir, `${Date.now()}${extension}`);
    await writeFile(filePath, Buffer.from(matched[2], "base64"));
    return { ok: true, filePath, cleanupDir: dir, source: imageConfig?.name || "uploaded-image" };
  }

  if (/^https?:\/\//i.test(source)) {
    const response = await fetch(source).catch((error) => {
      throw new Error(`image-download-failed:${error instanceof Error ? error.message : String(error)}`);
    });
    if (!response.ok) {
      return { ok: false, reason: `image-download-failed:${response.status}` };
    }

    const contentType = response.headers.get("content-type") || "";
    if (contentType && !contentType.toLowerCase().startsWith("image/")) {
      return { ok: false, reason: "downloaded-file-not-image" };
    }

    const extension = contentType.includes("png")
      ? ".png"
      : contentType.includes("webp")
        ? ".webp"
        : contentType.includes("gif")
          ? ".gif"
          : ".jpg";
    const dir = await mkdtemp(path.join(os.tmpdir(), "wangwang-image-"));
    const filePath = path.join(dir, `${Date.now()}${extension}`);
    await writeFile(filePath, Buffer.from(await response.arrayBuffer()));
    return { ok: true, filePath, cleanupDir: dir, source };
  }

  const filePath = path.isAbsolute(source) ? source : path.resolve(process.cwd(), source);
  if (!existsSync(filePath)) {
    return { ok: false, reason: "image-file-not-found" };
  }

  return { ok: true, filePath, cleanupDir: "", source };
}

async function findImageUploadInput(frame) {
  const candidates = [
    frame.locator('input[type="file"][accept*="image"]').first(),
    frame.locator('input[type="file"]').first(),
  ];

  for (const locator of candidates) {
    if ((await locator.count()) > 0) {
      return locator;
    }
  }

  return null;
}

async function uploadImageToChat(frame, imagePath) {
  const uploadInput = await findImageUploadInput(frame);
  if (!uploadInput) {
    return { ok: false, reason: "image-upload-input-not-found" };
  }

  await uploadInput.setInputFiles(imagePath).catch((error) => {
    throw new Error(`image-upload-failed:${error instanceof Error ? error.message : String(error)}`);
  });
  await antiDetectPause();
  return { ok: true };
}

async function compressImageForWangwangUpload(page, imagePath, maxBytes = WANGWANG_IMAGE_UPLOAD_MAX_BYTES) {
  const imageStat = await stat(imagePath).catch(() => null);
  if (!imageStat || imageStat.size <= maxBytes) {
    return { ok: true, filePath: imagePath, cleanupDir: "", compressed: false, sizeBytes: imageStat?.size ?? 0 };
  }

  const imageBuffer = await readFile(imagePath);
  const extension = path.extname(imagePath).toLowerCase();
  const mimeType = extension === ".webp"
    ? "image/webp"
    : extension === ".gif"
      ? "image/gif"
      : extension === ".jpg" || extension === ".jpeg"
        ? "image/jpeg"
        : "image/png";

  const compressed = await page.evaluate(
    async ({ base64, mimeType: sourceMimeType, maxBytes: targetMaxBytes }) => {
      const loadImage = (src) => new Promise((resolve, reject) => {
        const image = new Image();
        image.onload = () => resolve(image);
        image.onerror = () => reject(new Error("image-load-failed"));
        image.src = src;
      });
      const blobFromCanvas = (canvas, quality) => new Promise((resolve) => {
        canvas.toBlob((blob) => resolve(blob), "image/jpeg", quality);
      });
      const dataUrl = `data:${sourceMimeType};base64,${base64}`;
      const image = await loadImage(dataUrl);
      const canvas = document.createElement("canvas");
      const ctx = canvas.getContext("2d");
      if (!ctx) return { ok: false, reason: "canvas-context-unavailable" };

      let scale = 1;
      for (let attempt = 0; attempt < 12; attempt += 1) {
        canvas.width = Math.max(1, Math.round(image.naturalWidth * scale));
        canvas.height = Math.max(1, Math.round(image.naturalHeight * scale));
        ctx.fillStyle = "#fff";
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.drawImage(image, 0, 0, canvas.width, canvas.height);

        for (const quality of [0.9, 0.82, 0.74, 0.66, 0.58]) {
          const blob = await blobFromCanvas(canvas, quality);
          if (!blob) continue;
          if (blob.size <= targetMaxBytes || (attempt === 11 && quality === 0.58)) {
            const resultBase64 = await new Promise((resolve) => {
              const reader = new FileReader();
              reader.onloadend = () => resolve(String(reader.result).split(",")[1] || "");
              reader.readAsDataURL(blob);
            });
            return {
              ok: true,
              base64: resultBase64,
              sizeBytes: blob.size,
              width: canvas.width,
              height: canvas.height,
              quality,
            };
          }
        }

        scale *= 0.82;
      }

      return { ok: false, reason: "image-compress-failed" };
    },
    { base64: imageBuffer.toString("base64"), mimeType, maxBytes },
  );

  if (!compressed.ok) {
    return { ok: false, reason: compressed.reason ?? "image-compress-failed" };
  }

  const dir = await mkdtemp(path.join(os.tmpdir(), "wangwang-image-compressed-"));
  const filePath = path.join(dir, `${Date.now()}.jpg`);
  await writeFile(filePath, Buffer.from(compressed.base64, "base64"));
  return {
    ok: true,
    filePath,
    cleanupDir: dir,
    compressed: true,
    sizeBytes: compressed.sizeBytes,
    width: compressed.width,
    height: compressed.height,
  };
}

async function getVisibleChatError(frame) {
  const bodyText = await frame.locator("body").innerText().catch(() => "");
  if (bodyText.includes("图片超3M")) return "image-over-3mb";
  if (bodyText.includes("您尚未选择联系人")) return "contact-not-selected";
  if (bodyText.includes("发送失败")) return "send-failed";
  if (bodyText.includes("上传失败")) return "image-upload-failed";
  return "";
}

async function confirmImageMessageSent(frame, optionalText) {
  const expectedText = optionalText.trim();
  if (!expectedText) {
    return { ok: !(await getVisibleChatError(frame)), reason: await getVisibleChatError(frame) };
  }

  const confirmed = await frame.waitForFunction(
    (text) => {
      const editor = document.querySelector('[contenteditable="true"], textarea');
      const editorText = editor?.innerText || editor?.textContent || editor?.value || "";
      return !editorText.trim() && document.body?.innerText?.includes(text);
    },
    expectedText,
    { timeout: 10_000 },
  ).then(() => true).catch(() => false);

  if (confirmed) {
    return { ok: true };
  }

  return { ok: false, reason: (await getVisibleChatError(frame)) || "message-not-confirmed-in-frame" };
}

// ══════════════════════════════════════════════════════
//  会话创建 & 选择 (via _imsdk)
// ══════════════════════════════════════════════════════

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

/**
 * 通过 _imsdk 创建并选中会话
 */
export 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 };
}

// ══════════════════════════════════════════════════════
//  完整的"打开旺旺 → 发送消息"流程
// ══════════════════════════════════════════════════════

/**
 * 联系商家：打开旺旺链接 → 进入聊天 → 发送消息
 *
 * @param {import("playwright").BrowserContext} context
 * @param {object} result  搜索结果 (含 contact.url / contact.handle 等)
 * @param {string} askMessage  要发送的消息
 * @returns {Promise<{status: string, reason?: string, finalUrl?: string, conversationCode?: string}>}
 */
export 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 waitForChatUiReady(frame);
    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 sendResult = await sendChatMessage(frame, page, askMessage);
    if (!sendResult.ok) {
      return { status: "skipped", reason: sendResult.reason, finalUrl: page.url() };
    }

    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(() => {});
  }
}

// ══════════════════════════════════════════════════════
//  消息抓取（新增能力）
// ══════════════════════════════════════════════════════

/**
 * 计算消息文本的 MD5 哈希
 */
export function getMessageHash(text) {
  return crypto.createHash("md5").update(text || "").digest("hex");
}

/**
 * 判断是否有新回复
 */
export function hasNewReply(currentHash, lastStoredHash) {
  if (!lastStoredHash) return !!currentHash;
  return currentHash !== lastStoredHash;
}

/**
 * 从旺旺 iframe 抓取最新消息列表
 *
 * 注意：真实 DOM 选择器需根据实际页面调试确认，下面提供多种备选策略。
 * 在 headless=false 时先手动验证选择器是否命中。
 *
 * @param {import("playwright").Frame} frame  旺旺聊天 iframe
 * @returns {Promise<{messages: Array<{text: string, sender: "self"|"merchant"|"unknown"}>, latestHash: string}>}
 */
export async function scrapeLatestMessages(frame) {
  const extracted = await frame.evaluate(() => {
    // 策略 1：按照常见的 IM 气泡 DOM 结构抓取
    // 旺旺 IM 的消息通常在某个滚动容器内，每条消息有"self"和"other"区分
    const messages = [];

    // 尝试多种可能的选择器
    const selectors = [
      // 常见的 1688 旺旺 IM 消息选择器
      ".msg-content",
      ".message-item",
      "[class*='message']",
      "[class*='chat-msg']",
      "[class*='bubble']",
    ];

    let msgElements = [];
    for (const selector of selectors) {
      const els = document.querySelectorAll(selector);
      if (els.length > 0) {
        msgElements = Array.from(els);
        break;
      }
    }

    // 如果找到了消息元素
    for (const el of msgElements.slice(-10)) {
      const text = (el.textContent || "").replace(/\s+/g, " ").trim();
      if (!text) continue;

      // 判断发送者：检查父级/自身是否有 self/mine 相关 class
      const classes = (el.className || "") + " " + (el.parentElement?.className || "");
      let sender = "unknown";
      if (/self|mine|right|send/i.test(classes)) {
        sender = "self";
      } else if (/other|peer|left|receive/i.test(classes)) {
        sender = "merchant";
      }

      messages.push({ text, sender });
    }

    // 降级策略：如果上面没找到任何消息，尝试从 body 文本粗粒度提取
    if (messages.length === 0) {
      const bodyText = (document.body?.innerText || "").trim();
      if (bodyText) {
        messages.push({ text: bodyText, sender: "unknown" });
      }
    }

    return messages;
  });

  const lastMerchantMsg = [...extracted].reverse().find((m) => m.sender === "merchant");
  const lastAnyMsg = extracted[extracted.length - 1];
  const hashTarget = lastMerchantMsg?.text || lastAnyMsg?.text || "";

  return {
    messages: extracted,
    latestMerchantMessage: lastMerchantMsg || null,
    latestHash: getMessageHash(hashTarget),
  };
}

/**
 * 在已有会话中发送消息（用于多轮对话的第 2-4 轮）
 *
 * 与 contactMerchant 的区别：不重新创建会话，而是打开旺旺后直接在当前会话发消息。
 * 前提是旺旺 iframe 中已经选中了目标会话（通过 conversationCode 或联系人列表）。
 *
 * @param {import("playwright").BrowserContext} context
 * @param {object} chatTask  customer_service_chats 行
 * @param {string} messageText  要发送的文本
 * @returns {Promise<{status: string, reason?: string, finalUrl?: string}>}
 */
export async function sendMessageInExistingChat(context, chatTask, messageText) {
  if (!chatTask.contact_url) {
    return { status: "skipped", reason: "missing-contact-url" };
  }

  const contactIdentity = resolveChatTaskContactIdentity(chatTask);

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

    if (isLoginUrl(page.url())) {
      await promptForManualLogin(page, chatTask.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 waitForChatUiReady(frame);
    await dismissChatGuide(frame);

    // 尝试通过联系人列表选中目标会话
    const candidateTexts = [contactIdentity.handle, chatTask.merchant_name].filter(Boolean);
    let selected = false;
    for (const text of candidateTexts) {
      const item = frame.getByText(text, { exact: false }).first();
      if (await clickIfVisible(item, { force: true })) {
        await antiDetectPause();
        selected = true;
        break;
      }
    }

    // 如果联系人列表中没找到，尝试创建会话
    if (!selected) {
      const fakeResult = {
        contact: {
          handle: contactIdentity.handle,
          site: contactIdentity.site,
          url: chatTask.contact_url,
        },
        merchantName: chatTask.merchant_name,
      };
      const convStatus = await createAndSelectConversation(frame, fakeResult);
      if (!convStatus.ok) {
        return { status: "skipped", reason: convStatus.reason ?? "conversation-not-found", finalUrl: page.url() };
      }
    }

    const sendResult = await sendChatMessage(frame, page, messageText);
    if (!sendResult.ok) {
      return { status: "skipped", reason: sendResult.reason, finalUrl: page.url() };
    }

    return { status: "sent", finalUrl: page.url() };
  } catch (error) {
    return {
      status: "error",
      reason: error instanceof Error ? error.message : String(error),
      finalUrl: page.url(),
    };
  } finally {
    await page.close().catch(() => {});
  }
}

/**
 * 在已有会话中发送图片，支持本地路径或可下载 URL。
 *
 * @param {import("playwright").BrowserContext} context
 * @param {object} chatTask  customer_service_chats 行
 * @param {object} imageConfig  图片配置
 * @param {string} optionalText  可选随图文本
 * @returns {Promise<{status: string, reason?: string, finalUrl?: string, source?: string}>}
 */
export async function sendImageInExistingChat(context, chatTask, imageConfig, optionalText = "") {
  if (!chatTask.contact_url) {
    return { status: "skipped", reason: "missing-contact-url" };
  }

  const resolvedImage = await resolveImageFile(imageConfig).catch((error) => ({
    ok: false,
    reason: error instanceof Error ? error.message : String(error),
  }));
  if (!resolvedImage.ok) {
    return { status: "skipped", reason: resolvedImage.reason };
  }

  const contactIdentity = resolveChatTaskContactIdentity(chatTask);
  const page = await context.newPage();
  let uploadImage = null;
  try {
    await page.goto(chatTask.contact_url, { waitUntil: "domcontentloaded" });
    await antiDetectPause();

    if (isLoginUrl(page.url())) {
      await promptForManualLogin(page, chatTask.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(), source: resolvedImage.source };
    }

    await waitForChatUiReady(frame);
    await dismissChatGuide(frame);

    const candidateTexts = [contactIdentity.handle, chatTask.merchant_name].filter(Boolean);
    let selected = false;
    for (const text of candidateTexts) {
      const item = frame.getByText(text, { exact: false }).first();
      if (await clickIfVisible(item, { force: true })) {
        await antiDetectPause();
        selected = true;
        break;
      }
    }

    if (!selected) {
      const fakeResult = {
        contact: {
          handle: contactIdentity.handle,
          site: contactIdentity.site,
          url: chatTask.contact_url,
        },
        merchantName: chatTask.merchant_name,
      };
      const convStatus = await createAndSelectConversation(frame, fakeResult);
      if (!convStatus.ok) {
        return { status: "skipped", reason: convStatus.reason ?? "conversation-not-found", finalUrl: page.url(), source: resolvedImage.source };
      }
    }

    uploadImage = await compressImageForWangwangUpload(page, resolvedImage.filePath);
    if (!uploadImage.ok) {
      return { status: "skipped", reason: uploadImage.reason, finalUrl: page.url(), source: resolvedImage.source };
    }
    if (uploadImage.compressed) {
      console.log(`     🖼️ 图片已压缩至 ${Math.round(uploadImage.sizeBytes / 1024)}KB 后发送`);
    }

    const uploadResult = await uploadImageToChat(frame, uploadImage.filePath);
    if (!uploadResult.ok) {
      return { status: "skipped", reason: uploadResult.reason, finalUrl: page.url(), source: resolvedImage.source };
    }

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

    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 confirmed = await confirmImageMessageSent(frame, optionalText);
    if (!confirmed.ok) {
      return { status: "skipped", reason: confirmed.reason, finalUrl: page.url(), source: resolvedImage.source };
    }

    return { status: "sent", finalUrl: page.url(), source: resolvedImage.source };
  } catch (error) {
    return {
      status: "error",
      reason: error instanceof Error ? error.message : String(error),
      finalUrl: page.url(),
      source: resolvedImage.source,
    };
  } finally {
    if (resolvedImage.cleanupDir) {
      await rm(resolvedImage.cleanupDir, { recursive: true, force: true }).catch(() => {});
    }
    if (uploadImage?.cleanupDir) {
      await rm(uploadImage.cleanupDir, { recursive: true, force: true }).catch(() => {});
    }
    await page.close().catch(() => {});
  }
}

/**
 * 打开旺旺并抓取最新消息（不发送，只读）
 *
 * @param {import("playwright").BrowserContext} context
 * @param {object} chatTask  customer_service_chats 行
 * @returns {Promise<{ok: boolean, messages?: object[], latestHash?: string, latestMerchantMessage?: object, reason?: string}>}
 */
export async function openAndScrapeMessages(context, chatTask) {
  if (!chatTask.contact_url) {
    return { ok: false, reason: "missing-contact-url" };
  }

  const contactIdentity = resolveChatTaskContactIdentity(chatTask);

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

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

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

    if (!frame) {
      return { ok: false, reason: "chat-frame-not-found" };
    }

    await waitForChatUiReady(frame);
    await dismissChatGuide(frame);

    // 选中目标会话
    const candidateTexts = [contactIdentity.handle, chatTask.merchant_name].filter(Boolean);
    for (const text of candidateTexts) {
      const item = frame.getByText(text, { exact: false }).first();
      if (await clickIfVisible(item, { force: true })) {
        await antiDetectPause();
        break;
      }
    }

    const result = await scrapeLatestMessages(frame);
    return { ok: true, ...result };
  } catch (error) {
    return { ok: false, reason: error instanceof Error ? error.message : String(error) };
  } finally {
    await page.close().catch(() => {});
  }
}
