export const DEFAULT_COLLECTION_MCP_URL =
  "https://middle-aitool.gbotai.cn/mcp";
const MAX_MCP_ATTEMPTS = 10;

export type CollectionMcpBindings = {
  AI_TOOL_CENTER_MCP_URL?: string;
  AI_TOOL_CENTER_MCP_KEY?: string;
};

export type CollectionMcpConfig = {
  endpoint: string;
  key?: string;
  timeoutMs?: number;
};

export type XhsPublicMetrics = {
  likes: number;
  comments: number;
  collects: number;
  shares: number;
};

export type XhsAccountProfile = {
  platformUid: string;
  nickname: string;
  profileUrl: string;
  redId: string;
  ipLocation: string;
  followers: number | null;
  gender: "" | "男" | "女";
  bio: string;
  recentNoteTitles: string[];
  providerTags: string[];
};

type JsonRpcEnvelope = {
  error?: {
    code?: number;
    message?: string;
  };
  result?: {
    isError?: boolean;
    content?: Array<{
      type?: string;
      text?: string;
    }>;
    serverInfo?: {
      name?: string;
      version?: string;
    };
  };
};

type ToolResult = {
  isError: boolean;
  payload: unknown;
};

class McpSessionLostError extends Error {}

function isRetryableTransportError(error: unknown) {
  return (
    error instanceof McpSessionLostError ||
    (error instanceof Error &&
      ["AbortError", "TimeoutError"].includes(error.name)) ||
    error instanceof TypeError
  );
}

function asRecord(value: unknown): Record<string, unknown> | null {
  return value !== null && typeof value === "object" && !Array.isArray(value)
    ? (value as Record<string, unknown>)
    : null;
}

function safeMessage(value: unknown, fallback: string) {
  const message = String(value ?? "").trim();
  if (!message) return fallback;
  return message
    .replace(/https?:\/\/[^\s"'<>]+/gi, "[链接]")
    .replace(/eyJ[A-Za-z0-9._-]+/g, "[密钥]")
    .slice(0, 240);
}

function metricValue(value: unknown, label: string) {
  if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
    return Math.round(value);
  }
  if (typeof value !== "string") {
    throw new Error(`采集结果缺少${label}`);
  }

  const normalized = value.trim().toLowerCase().replace(/[,\s]/g, "");
  const match = normalized.match(/^(\d+(?:\.\d+)?)(万|w|千|k)?$/);
  if (!match) throw new Error(`采集结果中的${label}格式异常`);
  const multiplier =
    match[2] === "万" || match[2] === "w"
      ? 10_000
      : match[2] === "千" || match[2] === "k"
        ? 1_000
        : 1;
  return Math.round(Number(match[1]) * multiplier);
}

function optionalCountValue(value: unknown) {
  if (value === null || value === undefined || value === "") return null;
  const normalized =
    typeof value === "string" ? value.trim().replace(/\+$/, "") : value;
  try {
    return metricValue(normalized, "粉丝数");
  } catch {
    return null;
  }
}

function parseJsonRpc(raw: string) {
  const messages = raw
    .split(/\r?\n/)
    .filter((line) => line.startsWith("data:"))
    .map((line) => line.slice(5).trim())
    .filter(Boolean);
  const candidate = messages.at(-1) ?? raw.trim();
  if (!candidate) throw new Error("MCP采集服务返回空响应");
  try {
    return JSON.parse(candidate) as JsonRpcEnvelope;
  } catch {
    throw new Error("MCP采集服务返回了无法解析的数据");
  }
}

function buildMcpUrl(config: CollectionMcpConfig) {
  let endpoint: URL;
  try {
    endpoint = new URL(config.endpoint);
  } catch {
    throw new Error("MCP采集服务地址无效");
  }
  if (!["http:", "https:"].includes(endpoint.protocol)) {
    throw new Error("MCP采集服务地址无效");
  }
  if (config.key?.trim()) {
    endpoint.searchParams.set("key", config.key.trim());
  }
  if (!endpoint.searchParams.get("key")) {
    throw new Error("MCP采集密钥未配置");
  }
  return endpoint.toString();
}

function requestHeaders(sessionId?: string) {
  const headers = new Headers({
    "Content-Type": "application/json",
    Accept: "application/json, text/event-stream",
  });
  if (sessionId) headers.set("Mcp-Session-Id", sessionId);
  return headers;
}

async function postMcp(
  fetchImpl: typeof fetch,
  endpoint: string,
  body: Record<string, unknown>,
  timeoutMs: number,
  sessionId?: string,
  allowEmpty = false,
) {
  const response = await fetchImpl(endpoint, {
    method: "POST",
    headers: requestHeaders(sessionId),
    body: JSON.stringify(body),
    signal: AbortSignal.timeout(timeoutMs),
  });
  const text = await response.text();
  if (!response.ok) {
    if (/session not found|missing session id/i.test(text)) {
      throw new McpSessionLostError("MCP采集会话已失效");
    }
    let providerMessage: unknown = text;
    try {
      providerMessage =
        (JSON.parse(text) as JsonRpcEnvelope).error?.message ?? text;
    } catch {
      // Keep the original body when the upstream response is not JSON.
    }
    throw new Error(
      safeMessage(
        providerMessage,
        `MCP采集服务请求失败（HTTP ${response.status}）`,
      ),
    );
  }
  if (allowEmpty && !text.trim()) {
    return { response, envelope: null };
  }
  const envelope = parseJsonRpc(text);
  if (envelope.error) {
    const message = safeMessage(
      envelope.error.message,
      "MCP采集服务调用失败",
    );
    if (/session not found|missing session id/i.test(message)) {
      throw new McpSessionLostError("MCP采集会话已失效");
    }
    throw new Error(message);
  }
  return { response, envelope };
}

async function createMcpSession(
  fetchImpl: typeof fetch,
  endpoint: string,
  timeoutMs: number,
) {
  const initialize = await postMcp(
    fetchImpl,
    endpoint,
    {
      jsonrpc: "2.0",
      id: crypto.randomUUID(),
      method: "initialize",
      params: {
        protocolVersion: "2025-03-26",
        capabilities: {},
        clientInfo: {
          name: "koc-loop-collector",
          version: "1.0.0",
        },
      },
    },
    timeoutMs,
  );
  const sessionId = initialize.response.headers.get("mcp-session-id");
  if (!sessionId) return undefined;

  await postMcp(
    fetchImpl,
    endpoint,
    {
      jsonrpc: "2.0",
      method: "notifications/initialized",
      params: {},
    },
    timeoutMs,
    sessionId,
    true,
  );
  return sessionId;
}

async function invokeMcpTool(
  fetchImpl: typeof fetch,
  endpoint: string,
  sessionId: string | undefined,
  timeoutMs: number,
  name: string,
  args: Record<string, unknown>,
  allowText = false,
): Promise<ToolResult> {
  const result = await postMcp(
    fetchImpl,
    endpoint,
    {
      jsonrpc: "2.0",
      id: crypto.randomUUID(),
      method: "tools/call",
      params: {
        name,
        arguments: args,
      },
    },
    timeoutMs,
    sessionId,
  );
  const content = result.envelope?.result?.content ?? [];
  const text = content.find((block) => block.type === "text")?.text;
  if (!text) throw new Error("MCP采集工具未返回数据");
  let payload: unknown;
  try {
    payload = JSON.parse(text);
  } catch {
    if (allowText) {
      return {
        isError: result.envelope?.result?.isError === true,
        payload: { text },
      };
    }
    if (result.envelope?.result?.isError === true) {
      return {
        isError: true,
        payload: { message: text },
      };
    }
    throw new Error("MCP采集工具返回了无法解析的数据");
  }
  return {
    isError: result.envelope?.result?.isError === true,
    payload,
  };
}

function isToolArgumentShapeError(result: ToolResult) {
  if (!result.isError) return false;
  const root = asRecord(result.payload);
  const message = String(root?.message ?? "");
  return /input validation error/i.test(message);
}

async function callMcpTool(
  fetchImpl: typeof fetch,
  endpoint: string,
  sessionId: string | undefined,
  timeoutMs: number,
  name: string,
  args: Record<string, unknown>,
  allowText = false,
): Promise<ToolResult> {
  const nested = await invokeMcpTool(
    fetchImpl,
    endpoint,
    sessionId,
    timeoutMs,
    name,
    { request: args },
    allowText,
  );
  if (!isToolArgumentShapeError(nested)) return nested;
  return invokeMcpTool(
    fetchImpl,
    endpoint,
    sessionId,
    timeoutMs,
    name,
    args,
    allowText,
  );
}

function imageToolText(value: unknown) {
  if (typeof value === "string") return value;
  const root = asRecord(value);
  if (!root) return "";
  return [root.text, root.description, root.content, root.message, root.error]
    .flatMap((item) => (Array.isArray(item) ? item : [item]))
    .map((item) => {
      if (typeof item === "string") return item;
      const record = asRecord(item);
      return String(record?.text ?? record?.description ?? "");
    })
    .filter(Boolean)
    .join("\n");
}

function metricFromImageText(text: string, labels: string[], label: string) {
  const pattern = labels.join("|");
  const match = text.match(
    new RegExp(`(?:${pattern})\\s*[：:]?\\s*([\\d,.]+(?:万|w|千|k)?)`, "i"),
  );
  if (!match) return null;
  try {
    return metricValue(match[1], label);
  } catch {
    return null;
  }
}

function creatorMetricsFromImageResult(result: ToolResult) {
  if (result.isError) {
    const detail = imageToolText(result.payload);
    throw new Error(
      `图片识别 MCP 调用失败${detail ? `：${safeMessage(detail, "")}` : ""}`,
    );
  }
  const text = imageToolText(result.payload);
  const exposure = metricFromImageText(
    text,
    ["曝光量", "曝光", "impressions", "exposure"],
    "曝光量",
  );
  const views = metricFromImageText(
    text,
    ["阅读量", "阅读", "views", "view_count", "view count"],
    "阅读量",
  );
  if (exposure === null || views === null) {
    throw new Error("图片识别未找到曝光量和阅读量");
  }
  return { exposure, views };
}

export async function extractCreatorMetricsFromMcp(
  imageUrl: string,
  config: CollectionMcpConfig,
  fetchImpl: typeof fetch = fetch,
) {
  const endpoint = buildMcpUrl(config);
  const timeoutMs = Math.max(5_000, config.timeoutMs ?? 30_000);
  const sessionId = await createMcpSession(fetchImpl, endpoint, timeoutMs);
  const result = await callMcpTool(
    fetchImpl,
    endpoint,
    sessionId,
    timeoutMs,
    "analyze_image",
    { image_url: imageUrl },
    true,
  );
  return creatorMetricsFromImageResult(result);
}

function metricsFromToolResult(
  result: ToolResult,
  toolName = "fetch_content_detail",
): XhsPublicMetrics {
  const root = asRecord(result.payload);
  const response = asRecord(root?.response) ?? root;
  const data = asRecord(response?.data) ?? asRecord(root?.data);
  const code = Number(response?.code);
  const success = response?.success;

  if (
    result.isError ||
    success === false ||
    (Number.isFinite(code) && code >= 400)
  ) {
    const providerCode = Number(response?.code);
    const codeLabel = Number.isFinite(providerCode)
      ? `（code ${providerCode}）`
      : "";
    throw new Error(
      `MCP工具 ${toolName} 返回失败${codeLabel}：${safeMessage(
        response?.msg ?? response?.message ?? root?.message,
        "公开数据采集失败",
      )}`,
    );
  }
  if (!data) throw new Error(`MCP工具 ${toolName} 未返回互动数据`);

  const count = (value: unknown, label: string) =>
    value === null || value === undefined || value === ""
      ? 0
      : metricValue(value, label);
  return {
    likes: count(
      data.likes ??
        data.liked_count ??
        data.likedCount ??
        data.like_count ??
        data.likeCount ??
        data.digg_count ??
        data.diggCount,
      "点赞数",
    ),
    comments: count(
      data.comments ?? data.comment_count ?? data.commentCount,
      "评论数",
    ),
    collects: count(
      data.collects ??
        data.collected_count ??
        data.collectedCount ??
        data.favorites ??
        data.favourites ??
        data.collect_count ??
        data.collectCount,
      "收藏数",
    ),
    shares: count(
      data.shares ??
        data.share_count ??
        data.shareCount ??
        data.forwards ??
        data.forward_count ??
        data.forwardCount,
      "转发数",
    ),
  };
}

function usableDouyinSecUid(value: unknown) {
  const candidate = stringValue(value);
  return candidate && !/^\d+$/.test(candidate) && /^[A-Za-z0-9_-]{20,220}$/.test(candidate)
    ? candidate
    : "";
}

function verifiedDouyinProfileUrl(value: unknown) {
  const candidate = stringValue(value);
  if (!candidate) return "";
  try {
    const parsed = new URL(candidate);
    const secUid = decodeURIComponent(parsed.pathname.match(/^\/user\/([^/?#]+)/)?.[1] ?? "");
    if (
      parsed.protocol === "https:" &&
      (parsed.hostname === "douyin.com" || parsed.hostname.endsWith(".douyin.com")) &&
      usableDouyinSecUid(secUid)
    ) {
      return parsed.toString();
    }
  } catch {
    // The public redirect fallback below can still recover the profile URL.
  }
  return "";
}

async function douyinProfileFromPublicRedirect(
  publishUrl: string,
  fetchImpl: typeof fetch,
  timeoutMs: number,
) {
  let parsed: URL;
  try {
    parsed = new URL(publishUrl);
  } catch {
    return null;
  }
  if (
    parsed.protocol !== "https:" ||
    !(parsed.hostname === "douyin.com" || parsed.hostname.endsWith(".douyin.com"))
  ) {
    return null;
  }
  try {
    const response = await fetchImpl(parsed.toString(), {
      method: "GET",
      redirect: "manual",
      signal: AbortSignal.timeout(Math.min(timeoutMs, 15_000)),
      headers: {
        "user-agent":
          "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148",
      },
    });
    const location = response.headers.get("location");
    if (!location) return null;
    const redirectUrl = new URL(location, parsed);
    const secUid = usableDouyinSecUid(
      redirectUrl.searchParams.get("sec_uid") ??
        redirectUrl.searchParams.get("sec_user_id"),
    );
    return secUid
      ? {
          platformUid: secUid,
          profileUrl: `https://www.douyin.com/user/${encodeURIComponent(secUid)}`,
        }
      : null;
  } catch {
    return null;
  }
}

function successfulToolData(result: ToolResult, fallbackMessage: string) {
  const root = asRecord(result.payload);
  const response = asRecord(root?.response) ?? root;
  const data = asRecord(response?.data) ?? asRecord(root?.data);
  const code = Number(response?.code);
  if (
    result.isError ||
    response?.success === false ||
    (Number.isFinite(code) && code >= 400)
  ) {
    throw new Error(
      safeMessage(
        response?.msg ?? response?.message ?? root?.message,
        fallbackMessage,
      ),
    );
  }
  if (!data) throw new Error(fallbackMessage);
  return data;
}

function stringValue(value: unknown) {
  return typeof value === "string" ? value.trim() : "";
}

function findRecord(
  value: unknown,
  predicate: (record: Record<string, unknown>) => boolean,
  depth = 0,
): Record<string, unknown> | null {
  if (depth > 12) return null;
  if (Array.isArray(value)) {
    for (const item of value) {
      const found = findRecord(item, predicate, depth + 1);
      if (found) return found;
    }
    return null;
  }
  const record = asRecord(value);
  if (!record) return null;
  if (predicate(record)) return record;
  for (const child of Object.values(record)) {
    const found = findRecord(child, predicate, depth + 1);
    if (found) return found;
  }
  return null;
}

function findStringByKey(value: unknown, key: string, depth = 0): string {
  if (depth > 12) return "";
  if (Array.isArray(value)) {
    for (const item of value) {
      const found = findStringByKey(item, key, depth + 1);
      if (found) return found;
    }
    return "";
  }
  const record = asRecord(value);
  if (!record) return "";
  const direct = stringValue(record[key]);
  if (direct) return direct;
  for (const child of Object.values(record)) {
    const found = findStringByKey(child, key, depth + 1);
    if (found) return found;
  }
  return "";
}

function findValueByKeys(
  value: unknown,
  keys: ReadonlySet<string>,
  depth = 0,
): unknown {
  if (depth > 12) return undefined;
  if (Array.isArray(value)) {
    for (const item of value) {
      const found = findValueByKeys(item, keys, depth + 1);
      if (found !== undefined) return found;
    }
    return undefined;
  }
  const record = asRecord(value);
  if (!record) return undefined;
  for (const [key, child] of Object.entries(record)) {
    if (keys.has(key) && child !== null && child !== undefined && child !== "") {
      return child;
    }
  }
  for (const child of Object.values(record)) {
    const found = findValueByKeys(child, keys, depth + 1);
    if (found !== undefined) return found;
  }
  return undefined;
}

function profileGender(value: unknown): "" | "男" | "女" {
  if (value === 1) return "男";
  if (value === 2) return "女";
  const normalized = String(value ?? "").trim().toLocaleLowerCase("zh-CN");
  if (["男", "男性", "male", "m", "1"].includes(normalized)) return "男";
  if (["女", "女性", "female", "f", "2"].includes(normalized)) return "女";
  return "";
}

function recentNoteTitlesFromPayload(value: unknown) {
  const titles: string[] = [];
  const visit = (current: unknown, depth = 0) => {
    if (depth > 12 || titles.length >= 20) return;
    if (Array.isArray(current)) {
      current.forEach((item) => visit(item, depth + 1));
      return;
    }
    const record = asRecord(current);
    if (!record) return;
    const title = stringValue(record.title ?? record.note_title ?? record.noteTitle);
    if (
      title &&
      (record.note_id || record.noteId || record.url || record.cover) &&
      !titles.includes(title)
    ) {
      titles.push(title);
    }
    Object.values(record).forEach((child) => visit(child, depth + 1));
  };
  visit(value);
  return titles;
}

function providerTagsFromUser(value: unknown) {
  const user = findRecord(value, (record) =>
    Boolean(
      record.gender !== undefined ||
        record.desc !== undefined ||
        record.signature !== undefined ||
        record.fansCount !== undefined ||
        record.fans_count !== undefined,
    ),
  );
  const raw = user?.tags;
  if (!Array.isArray(raw)) return [];
  return [
    ...new Set(
      raw
        .map((item) =>
          typeof item === "string"
            ? item.trim()
            : stringValue(asRecord(item)?.name ?? asRecord(item)?.title),
        )
        .filter(Boolean),
    ),
  ].slice(0, 5);
}

const FOLLOWER_KEYS = new Set([
  "fans",
  "fans_count",
  "fansCount",
  "follower",
  "followers",
  "follower_count",
  "followerCount",
]);

function followerCountFromPayload(value: unknown, depth = 0): number | null {
  const direct = optionalCountValue(findValueByKeys(value, FOLLOWER_KEYS));
  if (direct !== null) return direct;
  if (typeof value === "string") {
    return optionalCountValue(
      value.match(/(?:粉丝数?|followers?|fans)\s*[：:]\s*([\d,.万千wkWＫＷ+]+)/i)?.[1],
    );
  }
  if (depth > 12) return null;
  if (Array.isArray(value)) {
    for (const item of value) {
      const found = followerCountFromPayload(item, depth + 1);
      if (found !== null) return found;
    }
    return null;
  }
  const record = asRecord(value);
  if (!record) return null;
  const label = stringValue(
    record.name ?? record.label ?? record.title ?? record.type,
  ).toLowerCase();
  if (["粉丝", "粉丝数", "fans", "followers"].includes(label)) {
    const labeled = optionalCountValue(
      record.count ?? record.value ?? record.i18nCount,
    );
    if (labeled !== null) return labeled;
  }
  for (const child of Object.values(record)) {
    const found = followerCountFromPayload(child, depth + 1);
    if (found !== null) return found;
  }
  return null;
}

export function xhsNoteIdFromUrl(input: string) {
  try {
    const url = new URL(input);
    if (
      !(
        url.hostname === "xiaohongshu.com" ||
        url.hostname.endsWith(".xiaohongshu.com")
      )
    ) {
      return "";
    }
    return (
      url.pathname.match(
        /\/(?:discovery\/item|explore)\/([A-Za-z0-9_-]{8,80})/,
      )?.[1] ?? ""
    );
  } catch {
    return "";
  }
}

async function xhsNoteIdFromShortLink(
  input: string,
  fallbackNickname: string,
  fetchImpl: typeof fetch,
  timeoutMs: number,
) {
  let url: URL;
  try {
    url = new URL(input);
  } catch {
    return { noteId: "", profile: null };
  }
  if (
    url.protocol !== "http:" &&
    url.protocol !== "https:"
  ) {
    return { noteId: "", profile: null };
  }
  const isShortLink =
    url.hostname === "xhslink.cn" ||
    url.hostname.endsWith(".xhslink.cn");
  const isXhsPage =
    url.hostname === "xiaohongshu.com" ||
    url.hostname.endsWith(".xiaohongshu.com");
  if (!isShortLink && !isXhsPage) {
    return { noteId: "", profile: null };
  }

  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const response = await fetchImpl(url.toString(), {
      method: "GET",
      redirect: "follow",
      signal: controller.signal,
      headers: {
        "user-agent":
          "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148",
      },
    });
    const location = response.headers.get("location");
    const resolvedUrl = location
      ? new URL(location, url).toString()
      : response.url;
    const noteId = xhsNoteIdFromUrl(resolvedUrl);
    const html =
      response.ok &&
      response.headers.get("content-type")?.includes("text/html")
        ? await response.text()
        : "";
    return {
      noteId,
      profile: accountProfileFromPublicPage(
        html,
        fallbackNickname,
      ),
    };
  } catch {
    return { noteId: "", profile: null };
  } finally {
    clearTimeout(timeout);
  }
}

function accountProfileFromPublicPage(
  html: string,
  fallbackNickname: string,
): XhsAccountProfile | null {
  if (!html) return null;
  const decoded = html
    .replaceAll("&quot;", '"')
    .replaceAll("&#34;", '"')
    .replaceAll("&amp;", "&")
    .replaceAll("&#39;", "'")
    .replaceAll("&lt;", "<")
    .replaceAll("&gt;", ">");
  const noteData = decoded.match(
    /"noteData":\{[\s\S]{0,30000}?"user":\{([\s\S]{0,1800}?)\}/,
  )?.[1];
  if (!noteData) return null;
  const platformUid =
    noteData.match(/"userId":"([A-Za-z0-9_-]{8,80})"/)?.[1] ?? "";
  if (!platformUid) return null;
  const nickname =
    noteData.match(/"(?:nickName|nickname)":"([^"]+)"/)?.[1] ??
    fallbackNickname.trim();
  const redId =
    noteData.match(/"(?:redId|red_id)":"([^"]+)"/)?.[1] ??
    decoded.match(/"(?:redId|red_id)":"([^"]+)"/)?.[1] ??
    decoded.match(/小红书号[：:]\s*([^<"\s]+)/)?.[1] ??
    "";
  return {
    platformUid,
    nickname,
    profileUrl: `https://www.xiaohongshu.com/user/profile/${encodeURIComponent(
      platformUid,
    )}`,
    redId,
    ipLocation: "待识别",
    followers: null,
    gender: "",
    bio: "",
    recentNoteTitles: [],
    providerTags: [],
  };
}

export async function resolveXhsPublicAccountDetails(
  profileUrl: string,
  fetchImpl: typeof fetch = fetch,
) {
  let parsed: URL;
  try {
    parsed = new URL(profileUrl);
  } catch {
    return { nickname: "", redId: "", followers: null, ipLocation: "" };
  }
  if (
    parsed.protocol !== "https:" ||
    !(
      parsed.hostname === "xiaohongshu.com" ||
      parsed.hostname.endsWith(".xiaohongshu.com")
    ) ||
    !parsed.pathname.startsWith("/user/profile/")
  ) {
    return { nickname: "", redId: "", followers: null, ipLocation: "" };
  }
  try {
    const response = await fetchImpl(parsed.toString(), {
      method: "GET",
      redirect: "follow",
      signal: AbortSignal.timeout(15_000),
      headers: {
        "user-agent":
          "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/138.0 Safari/537.36",
      },
    });
    if (!response.ok) {
      return { nickname: "", redId: "", followers: null, ipLocation: "" };
    }
    const html = await response.text();
    return {
      nickname:
        html.match(/"(?:nickname|nickName)":"([^"]+)"/)?.[1] ?? "",
      redId:
        html.match(/"(?:redId|red_id)":"([^"]+)"/)?.[1] ??
        html.match(/小红书号[：:]\s*([^<"\s]+)/)?.[1] ??
        "",
      followers:
        optionalCountValue(
          html.match(
            /"name"\s*:\s*"粉丝"\s*,\s*"count"\s*:\s*"([^"]+)"/,
          )?.[1],
        ) ??
        followerCountFromPayload(html),
      ipLocation: "",
    };
  } catch {
    return { nickname: "", redId: "", followers: null, ipLocation: "" };
  }
}

export async function resolveXhsPublicAccountId(
  profileUrl: string,
  fetchImpl: typeof fetch = fetch,
) {
  return (await resolveXhsPublicAccountDetails(profileUrl, fetchImpl)).redId;
}

function profileDetailsFromToolResult(result: ToolResult) {
  const root = asRecord(result.payload);
  const response = asRecord(root?.response) ?? root;
  const code = Number(response?.code);
  if (
    result.isError ||
    response?.success === false ||
    (Number.isFinite(code) && code >= 400)
  ) {
    throw new Error(
      safeMessage(
        response?.msg ?? response?.message ?? root?.message,
        "账号主页数据采集失败",
      ),
    );
  }
  const payload =
    asRecord(response?.data) ?? asRecord(root?.data) ?? result.payload;
  const followers = followerCountFromPayload(payload);
  const nickname =
    findStringByKey(payload, "nickname") ||
    findStringByKey(payload, "nickName");
  const redId =
    findStringByKey(payload, "red_id") ||
    findStringByKey(payload, "redId") ||
    findStringByKey(payload, "unique_id") ||
    findStringByKey(payload, "uniqueId") ||
    findStringByKey(payload, "short_id") ||
    findStringByKey(payload, "shortId") ||
    findStringByKey(payload, "douyin_id") ||
    findStringByKey(payload, "userId") ||
    findStringByKey(payload, "user_id");
  const ipLocation =
    findStringByKey(payload, "ip_location") ||
    findStringByKey(payload, "ipLocation");
  const gender = profileGender(findValueByKeys(payload, new Set(["gender", "sex"])));
  const bio =
    findStringByKey(payload, "desc") ||
    findStringByKey(payload, "description") ||
    findStringByKey(payload, "signature") ||
    findStringByKey(payload, "bio");
  const recentNoteTitles = recentNoteTitlesFromPayload(payload);
  const providerTags = providerTagsFromUser(payload);
  if (
    followers === null &&
    !nickname &&
    !redId &&
    !ipLocation &&
    !gender &&
    !bio &&
    recentNoteTitles.length === 0
  ) {
    throw new Error("账号主页采集结果缺少可用字段");
  }
  return {
    nickname,
    followers,
    redId,
    ipLocation,
    gender,
    bio,
    recentNoteTitles,
    providerTags,
  };
}

function accountProfileFromToolResult(
  result: ToolResult,
  fallbackNickname: string,
): XhsAccountProfile {
  const data = successfulToolData(result, "账号主页识别失败");
  const user = findRecord(
    data,
    (record) =>
      Boolean(
        stringValue(record.user_id ?? record.userid ?? record.userId) &&
          (stringValue(record.profile_url) ||
            stringValue(record.nickname ?? record.name)),
      ),
  );
  if (!user) throw new Error("账号主页识别结果缺少作者信息");
  const platformUid = stringValue(
    user.user_id ?? user.userid ?? user.userId,
  );
  const candidateProfileUrl = stringValue(
    user.profile_url ?? user.profileUrl,
  );
  let profileUrl = "";
  if (candidateProfileUrl) {
    try {
      const parsed = new URL(candidateProfileUrl);
      if (
        parsed.protocol === "https:" &&
        (parsed.hostname === "xiaohongshu.com" ||
          parsed.hostname.endsWith(".xiaohongshu.com")) &&
        parsed.pathname.startsWith("/user/profile/")
      ) {
        profileUrl = parsed.toString();
      }
    } catch {
      // Fall back to the verified user id below.
    }
  }
  if (!profileUrl) {
    profileUrl = `https://www.xiaohongshu.com/user/profile/${encodeURIComponent(
      platformUid,
    )}`;
  }
  return {
    platformUid,
    nickname:
      stringValue(user.nickname ?? user.name) || fallbackNickname.trim(),
    profileUrl,
    redId: stringValue(user.red_id),
    ipLocation: findStringByKey(data, "ip_location") || "待识别",
    followers: followerCountFromPayload(data),
    gender: "",
    bio: "",
    recentNoteTitles: [],
    providerTags: [],
  };
}

async function completeAccountProfile(
  profile: XhsAccountProfile,
  fetchImpl: typeof fetch,
  endpoint: string,
  sessionId: string | undefined,
  timeoutMs: number,
) {
  let completed = profile;
  for (const [name, args] of [
    [
      "parse_xhs_user_summary",
      { url: profile.profileUrl, use_proxy: true },
    ],
  ] as const) {
    try {
      const result = await callMcpTool(
        fetchImpl,
        endpoint,
        sessionId,
        timeoutMs,
        name,
        args,
      );
      const details = profileDetailsFromToolResult(result);
      completed = {
        ...completed,
        followers: details.followers ?? completed.followers,
        redId: details.redId || completed.redId,
        ipLocation:
          details.ipLocation && details.ipLocation !== "待识别"
            ? details.ipLocation
            : completed.ipLocation,
        gender: details.gender || completed.gender,
        bio: details.bio || completed.bio,
        recentNoteTitles:
          details.recentNoteTitles.length > 0
            ? details.recentNoteTitles
            : completed.recentNoteTitles,
        providerTags:
          details.providerTags.length > 0
            ? details.providerTags
            : completed.providerTags,
      };
    } catch (error) {
      if (error instanceof McpSessionLostError) throw error;
    }
  }

  if (completed.followers === null || !completed.redId) {
    const details = await resolveXhsPublicAccountDetails(
      completed.profileUrl,
      fetchImpl,
    );
    completed = {
      ...completed,
      followers: details.followers ?? completed.followers,
      redId: details.redId || completed.redId,
    };
  }
  return completed;
}

async function resolveProfileDetailsInSession(
  profileUrl: string,
  config: CollectionMcpConfig,
  fetchImpl: typeof fetch,
) {
  const endpoint = buildMcpUrl(config);
  const timeoutMs = Math.max(5_000, config.timeoutMs ?? 30_000);
  const sessionId = await createMcpSession(fetchImpl, endpoint, timeoutMs);
  const result = await callMcpTool(
    fetchImpl,
    endpoint,
    sessionId,
    timeoutMs,
    "parse_xhs_user_summary",
    {
      url: profileUrl,
      use_proxy: true,
    },
  );
  return profileDetailsFromToolResult(result);
}

async function resolveAccountInSession(
  publishUrl: string,
  fallbackNickname: string,
  config: CollectionMcpConfig,
  fetchImpl: typeof fetch,
) {
  const endpoint = buildMcpUrl(config);
  const timeoutMs = Math.max(5_000, config.timeoutMs ?? 30_000);
  let noteId = xhsNoteIdFromUrl(publishUrl);
  const linkPage = await xhsNoteIdFromShortLink(
    publishUrl,
    fallbackNickname,
    fetchImpl,
    timeoutMs,
  );
  noteId = noteId || linkPage.noteId;
  const publicPageProfile = linkPage.profile;

  try {
    const sessionId = await createMcpSession(
      fetchImpl,
      endpoint,
      timeoutMs,
    );
    const noteResult = await callMcpTool(
      fetchImpl,
      endpoint,
      sessionId,
      timeoutMs,
      "fetch_content_detail",
      {
        link: publishUrl,
        plant: "xhs",
        include_comments: false,
        auto_cookie: true,
      },
    );
    const noteData = successfulToolData(
      noteResult,
      "无法识别小红书笔记",
    );
    noteId =
      noteId ||
      stringValue(noteData.noteId ?? noteData.note_id) ||
      findStringByKey(noteData, "noteId") ||
      findStringByKey(noteData, "note_id");
    if (!noteId) throw new Error("无法从发布链接识别小红书笔记ID");
    let profile: XhsAccountProfile;
    try {
      profile = accountProfileFromToolResult(
        noteResult,
        fallbackNickname,
      );
    } catch {
      if (!publicPageProfile) throw new Error("笔记数据缺少公开作者主页");
      profile = publicPageProfile;
    }
    return completeAccountProfile(
      profile,
      fetchImpl,
      endpoint,
      sessionId,
      timeoutMs,
    );
  } catch (error) {
    if (error instanceof McpSessionLostError) throw error;
    if (publicPageProfile) {
      const details = await resolveXhsPublicAccountDetails(
        publicPageProfile.profileUrl,
        fetchImpl,
      );
      return {
        ...publicPageProfile,
        redId: details.redId || publicPageProfile.redId,
        followers: details.followers,
      };
    }
    throw error;
  }
}

async function collectInSession(
  publishUrl: string,
  platform: "小红书" | "抖音",
  config: CollectionMcpConfig,
  fetchImpl: typeof fetch,
) {
  const endpoint = buildMcpUrl(config);
  const timeoutMs = Math.max(5_000, config.timeoutMs ?? 30_000);
  const sessionId = await createMcpSession(fetchImpl, endpoint, timeoutMs);

  const primary = await callMcpTool(
    fetchImpl,
    endpoint,
    sessionId,
    timeoutMs,
    "fetch_content_detail",
    {
      link: publishUrl,
      plant: platform === "抖音" ? "dy" : "xhs",
      include_comments: false,
      auto_cookie: true,
    },
  );
  return metricsFromToolResult(primary, "fetch_content_detail");
}

export function resolveCollectionMcpConfig(
  bindings: CollectionMcpBindings,
): CollectionMcpConfig {
  return {
    endpoint:
      bindings.AI_TOOL_CENTER_MCP_URL?.trim() ||
      DEFAULT_COLLECTION_MCP_URL,
    key: bindings.AI_TOOL_CENTER_MCP_KEY?.trim(),
  };
}

export async function collectXhsMetricsFromMcp(
  publishUrl: string,
  config: CollectionMcpConfig,
  fetchImpl: typeof fetch = fetch,
) {
  let parsed: URL;
  try {
    parsed = new URL(publishUrl);
  } catch {
    throw new Error("发布链接无效");
  }
  if (!["http:", "https:"].includes(parsed.protocol)) {
    throw new Error("发布链接无效");
  }

  let lastError: unknown;
  for (let attempt = 0; attempt < MAX_MCP_ATTEMPTS; attempt += 1) {
    try {
      return await collectInSession(parsed.toString(), "小红书", config, fetchImpl);
    } catch (error) {
      lastError = error;
      if (!isRetryableTransportError(error)) throw error;
    }
  }
  if (
    lastError instanceof Error &&
    ["AbortError", "TimeoutError"].includes(lastError.name)
  ) {
    throw new Error("MCP采集服务响应超时，请稍后重试");
  }
  throw lastError instanceof Error
    ? lastError
    : new Error("MCP采集服务暂时不可用");
}

export async function collectMetricsFromMcp(
  publishUrl: string,
  platform: "小红书" | "抖音",
  config: CollectionMcpConfig,
  fetchImpl: typeof fetch = fetch,
) {
  if (platform === "小红书") {
    return collectXhsMetricsFromMcp(publishUrl, config, fetchImpl);
  }
  let parsed: URL;
  try {
    parsed = new URL(publishUrl);
  } catch {
    throw new Error("发布链接无效");
  }
  if (!["http:", "https:"].includes(parsed.protocol)) {
    throw new Error("发布链接无效");
  }
  let lastError: unknown;
  for (let attempt = 0; attempt < MAX_MCP_ATTEMPTS; attempt += 1) {
    try {
      return await collectInSession(parsed.toString(), platform, config, fetchImpl);
    } catch (error) {
      lastError = error;
      if (!isRetryableTransportError(error)) throw error;
    }
  }
  throw lastError instanceof Error
    ? lastError
    : new Error("MCP采集服务暂时不可用");
}

function douyinProfileFromToolResult(
  result: ToolResult,
  fallbackNickname: string,
): XhsAccountProfile {
  const data = successfulToolData(result, "无法识别抖音作品");
  const author = findRecord(data, (record) =>
    Boolean(
      stringValue(
        record.sec_uid ?? record.secUid ?? record.uid ?? record.user_id ?? record.userId,
      ) && stringValue(record.nickname ?? record.name ?? record.unique_id ?? record.uniqueId),
    ),
  );
  if (!author) throw new Error("抖音作品数据缺少作者信息");
  const verifiedSecUid = usableDouyinSecUid(author.sec_uid ?? author.secUid);
  const fallbackUid = stringValue(author.uid ?? author.user_id ?? author.userId);
  const platformUid = verifiedSecUid || fallbackUid;
  const publicId = stringValue(
    author.unique_id ?? author.uniqueId ?? author.short_id ?? author.shortId ?? author.douyin_id,
  );
  const candidateProfileUrl = verifiedDouyinProfileUrl(
    author.profile_url ?? author.profileUrl,
  );
  const profileUrl = candidateProfileUrl ||
    (verifiedSecUid
      ? `https://www.douyin.com/user/${encodeURIComponent(verifiedSecUid)}`
      : "");
  return {
    platformUid,
    nickname: stringValue(author.nickname ?? author.name) || fallbackNickname.trim(),
    profileUrl,
    redId: publicId,
    ipLocation:
      stringValue(author.ip_location ?? author.ipLocation) ||
      findStringByKey(data, "ip_location") ||
      findStringByKey(data, "ipLocation") ||
      "待识别",
    followers: followerCountFromPayload(author) ?? followerCountFromPayload(data),
    gender: profileGender(author.gender ?? author.sex),
    bio: stringValue(author.desc ?? author.description ?? author.signature ?? author.bio),
    recentNoteTitles: recentNoteTitlesFromPayload(data),
    providerTags: providerTagsFromUser(data),
  };
}

async function resolveDouyinAccountInSession(
  publishUrl: string,
  fallbackNickname: string,
  config: CollectionMcpConfig,
  fetchImpl: typeof fetch,
) {
  const endpoint = buildMcpUrl(config);
  const timeoutMs = Math.max(5_000, config.timeoutMs ?? 30_000);
  const sessionId = await createMcpSession(fetchImpl, endpoint, timeoutMs);
  const result = await callMcpTool(
    fetchImpl,
    endpoint,
    sessionId,
    timeoutMs,
    "fetch_content_detail",
    {
      link: publishUrl,
      plant: "dy",
      include_comments: false,
      auto_cookie: true,
    },
  );
  let profile = douyinProfileFromToolResult(result, fallbackNickname);
  if (!verifiedDouyinProfileUrl(profile.profileUrl)) {
    const resolved = await douyinProfileFromPublicRedirect(
      publishUrl,
      fetchImpl,
      timeoutMs,
    );
    if (resolved) {
      profile = {
        ...profile,
        platformUid: resolved.platformUid,
        profileUrl: resolved.profileUrl,
      };
    }
  }
  if (!verifiedDouyinProfileUrl(profile.profileUrl)) return profile;
  try {
    const detailsResult = await callMcpTool(
      fetchImpl,
      endpoint,
      sessionId,
      timeoutMs,
      "parse_dy_user_summary",
      { url: profile.profileUrl },
    );
    const details = profileDetailsFromToolResult(detailsResult);
    profile = {
      ...profile,
      nickname: details.nickname || profile.nickname,
      redId: details.redId || profile.redId,
      followers: details.followers ?? profile.followers,
      ipLocation:
        details.ipLocation && details.ipLocation !== "待识别"
          ? details.ipLocation
          : profile.ipLocation,
      gender: details.gender || profile.gender,
      bio: details.bio || profile.bio,
      recentNoteTitles:
        details.recentNoteTitles.length > 0
          ? details.recentNoteTitles
          : profile.recentNoteTitles,
      providerTags:
        details.providerTags.length > 0
          ? details.providerTags
          : profile.providerTags,
    };
  } catch (error) {
    if (error instanceof McpSessionLostError) throw error;
  }
  return profile;
}

export async function resolveAccountProfileFromMcp(
  publishUrl: string,
  fallbackNickname: string,
  platform: "小红书" | "抖音",
  config: CollectionMcpConfig,
  fetchImpl: typeof fetch = fetch,
) {
  if (platform === "小红书") {
    return resolveXhsAccountProfileFromMcp(
      publishUrl,
      fallbackNickname,
      config,
      fetchImpl,
    );
  }
  let lastError: unknown;
  for (let attempt = 0; attempt < MAX_MCP_ATTEMPTS; attempt += 1) {
    try {
      return await resolveDouyinAccountInSession(
        publishUrl,
        fallbackNickname,
        config,
        fetchImpl,
      );
    } catch (error) {
      lastError = error;
      if (!isRetryableTransportError(error)) throw error;
    }
  }
  throw lastError instanceof Error ? lastError : new Error("抖音账号识别失败");
}

export async function resolveProfileDetailsFromMcp(
  profileUrl: string,
  platform: "小红书" | "抖音",
  config: CollectionMcpConfig,
  fetchImpl: typeof fetch = fetch,
) {
  if (platform === "小红书") {
    return resolveXhsProfileDetailsFromMcp(profileUrl, config, fetchImpl);
  }
  const endpoint = buildMcpUrl(config);
  const timeoutMs = Math.max(5_000, config.timeoutMs ?? 30_000);
  const sessionId = await createMcpSession(fetchImpl, endpoint, timeoutMs);
  const result = await callMcpTool(
    fetchImpl,
    endpoint,
    sessionId,
    timeoutMs,
    "parse_dy_user_summary",
    { url: profileUrl },
  );
  return profileDetailsFromToolResult(result);
}

export async function resolveXhsAccountProfileFromMcp(
  publishUrl: string,
  fallbackNickname: string,
  config: CollectionMcpConfig,
  fetchImpl: typeof fetch = fetch,
) {
  let parsed: URL;
  try {
    parsed = new URL(publishUrl);
  } catch {
    throw new Error("发布链接无效");
  }
  if (!["http:", "https:"].includes(parsed.protocol)) {
    throw new Error("发布链接无效");
  }

  let lastError: unknown;
  for (let attempt = 0; attempt < MAX_MCP_ATTEMPTS; attempt += 1) {
    try {
      return await resolveAccountInSession(
        parsed.toString(),
        fallbackNickname,
        config,
        fetchImpl,
      );
    } catch (error) {
      lastError = error;
      if (!isRetryableTransportError(error)) throw error;
    }
  }
  throw lastError instanceof Error
    ? lastError
    : new Error("账号主页识别服务暂时不可用");
}

export async function resolveXhsProfileDetailsFromMcp(
  profileUrl: string,
  config: CollectionMcpConfig,
  fetchImpl: typeof fetch = fetch,
) {
  let parsed: URL;
  try {
    parsed = new URL(profileUrl);
  } catch {
    throw new Error("账号主页链接无效");
  }
  if (
    parsed.protocol !== "https:" ||
    !(
      parsed.hostname === "xiaohongshu.com" ||
      parsed.hostname.endsWith(".xiaohongshu.com")
    ) ||
    !parsed.pathname.startsWith("/user/profile/")
  ) {
    throw new Error("账号主页链接无效");
  }

  let lastError: unknown;
  for (let attempt = 0; attempt < MAX_MCP_ATTEMPTS; attempt += 1) {
    try {
      return await resolveProfileDetailsInSession(
        parsed.toString(),
        config,
        fetchImpl,
      );
    } catch (error) {
      lastError = error;
      if (!isRetryableTransportError(error)) throw error;
    }
  }
  throw lastError instanceof Error
    ? lastError
    : new Error("账号主页数据采集服务暂时不可用");
}
