import "dotenv/config";

import { access, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { pathToFileURL } from "node:url";
import type OpenAI from "openai";

import { createLlmClient, resolveStageLlmModel, type ResolvedLlmRuntimeConfig } from "../core/llm-client.ts";
import {
  getOptionalPayloadObject,
  getRequiredPayloadString,
  renderPromptTemplate,
  resolvePromptBundle,
} from "../internal/prompt-runtime.js";

type ComponentRoles = Record<string, unknown>;

type AiKeywordAnalysis = {
  reasoning?: string;
  suggested_keyword?: string;
  product_type?: string;
  structure_summary?: string;
  component_roles?: ComponentRoles;
  purchase_type?: string;
};

type ImageSearchReportInput = {
  material_id?: string;
  source_image_path?: string;
  image_search?: {
    screenshot_path?: string;
    top_titles?: unknown[];
  };
  similar_products?: Array<{
    title?: string;
  }>;
};

type KeywordProcessReport = {
  ai_keyword_analysis?: AiKeywordAnalysis | null;
};

type TermStat = {
  term: string;
  hits: number;
  ratio: number;
};

type TitleAnalysis = {
  material_terms: TermStat[];
  whole_product_terms: TermStat[];
  component_terms: TermStat[];
  structure_terms: TermStat[];
  ignored_terms: TermStat[];
};

type FinalResult = {
  主关键词: string;
  判断原因: string;
};

type DebugArchive = {
  generated_at: string;
  input_report_path: string;
  keyword_process_path: string;
  material_id: string;
  source_image_path: string;
  screenshot_path: string;
  top_titles: string[];
  ai_keyword_analysis: AiKeywordAnalysis | null;
  title_analysis: TitleAnalysis;
  model_used: string;
  is_fallback: boolean;
  fallback_reason: string;
  multimodal_raw_response: string;
  multimodal_parsed: Record<string, unknown> | null;
  final_result: FinalResult;
};

type FusionAnalysisParams = {
  client: OpenAI;
  aiAnalysis: AiKeywordAnalysis;
  model: string;
  sourceImagePath: string;
  screenshotPath: string;
  titles: string[];
  titleAnalysis: TitleAnalysis;
};

type RunKeywordSelectionOptions = {
  inputPath?: string;
  outputRoot?: string;
  keywordProcessPath?: string;
  createRunDir?: boolean;
  resultFileName?: string;
  debugFileName?: string;
};

type RunKeywordSelectionResult = {
  runDir: string;
  outputPath: string;
  debugPath: string;
  materialId: string;
  inputPath: string;
  keywordProcessPath: string;
  isFallback: boolean;
  fallbackReason: string;
  finalResult: FinalResult;
};

const AUTOMATION_DIR = path.resolve(import.meta.dirname!, "..");
const IMAGE_SEARCH_RUNS_ROOT = path.join(AUTOMATION_DIR, "output", "image-search-keywords");
const LINKED_ROW_RUNS_ROOT = path.join(AUTOMATION_DIR, "output", "linked-row-runs");
const OUTPUT_ROOT = path.join(AUTOMATION_DIR, "output", "image-keyword-tests");
const KEYWORD_FUSION_PROMPT_KEY = "automation.search.keyword_fusion.bundle";

const MATERIAL_HINTS = ["不锈钢", "钢", "铁", "铝", "铜", "玻璃", "木", "塑料", "塑胶", "橡胶", "石材", "陶瓷", "亚克力", "PVC", "ABS", "硅胶", "尼龙", "皮革", "布艺", "纸"];
const COMPONENT_HINTS = ["配件", "组件", "零件", "连接件", "挂件", "支架", "底座", "门", "锁", "螺丝", "接头", "立柱", "扶手", "面板", "杆", "柱", "片", "板", "框", "盖", "轴", "头", "轮", "座"];
const STRUCTURE_HINTS = ["折叠", "伸缩", "挂装", "嵌入", "拼接", "夹胶", "单片", "双层", "带孔", "异形", "加厚", "可调", "快装"];
const NOISE_HINTS = ["厂家", "批发", "定制", "现货", "直销", "工程", "家用", "商用", "学校", "商场", "室内", "室外", "阳台", "楼梯", "车站", "机场", "地铁", "平台"];

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

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

function cleanTitle(value: unknown) {
  return normalizeSpace(String(value ?? "").replace(/[｜|]+.*$/u, "").replace(/[“”"']/g, ""));
}

function ensureString(value: unknown, fallback = "") {
  const normalized = normalizeSpace(value);
  return normalized || fallback;
}

function ensureReadableText(value: unknown, fallback = "") {
  if (typeof value === "string") {
    return ensureString(value, fallback);
  }

  if (Array.isArray(value)) {
    const normalized = value.map((item) => ensureString(item)).filter(Boolean).join("；");
    return normalized || fallback;
  }

  if (value && typeof value === "object") {
    const normalized = Object.entries(value as Record<string, unknown>)
      .map(([key, item]) => `${key}：${ensureString(item)}`)
      .filter((item) => !item.endsWith("："))
      .join("；");
    return normalized || fallback;
  }

  return ensureString(value, fallback);
}

function compactText(value: string) {
  return ensureString(value).replace(/\s+/g, "");
}

function dedupeList(values: string[]) {
  const seen = new Set<string>();
  const output: string[] = [];

  for (const value of values) {
    const normalized = normalizeSpace(value);
    if (!normalized) continue;
    const key = normalized.toLowerCase();
    if (seen.has(key)) continue;
    seen.add(key);
    output.push(normalized);
  }

  return output;
}

function tryParseJsonObject(text: string) {
  if (!text) {
    return null;
  }

  const cleaned = text.replace(/```json\s*/g, "").replace(/```\s*/g, "").trim();
  const match = cleaned.match(/\{[\s\S]*\}/);
  if (!match) {
    return null;
  }

  try {
    return JSON.parse(match[0]) as Record<string, unknown>;
  } catch {
    return null;
  }
}

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

async function findLatestKeywordProcessPath(materialId: string) {
  const prefix = `${materialId}-`;
  const entries = await readdir(LINKED_ROW_RUNS_ROOT, { withFileTypes: true });
  const candidates = entries
    .filter((entry) => entry.isDirectory() && entry.name.startsWith(prefix))
    .map((entry) => path.join(LINKED_ROW_RUNS_ROOT, entry.name, "keyword-process.json"))
    .sort((left, right) => right.localeCompare(left, "zh-CN"));

  for (const candidate of candidates) {
    if (await fileExists(candidate)) {
      return candidate;
    }
  }

  return "";
}

async function findLatestImageSearchReportPath() {
  const entries = await readdir(IMAGE_SEARCH_RUNS_ROOT, { withFileTypes: true });
  const candidates = entries
    .filter((entry) => entry.isDirectory() && entry.name.startsWith("run-"))
    .map((entry) => path.join(IMAGE_SEARCH_RUNS_ROOT, entry.name, "image-search-keyword-report.json"))
    .sort((left, right) => right.localeCompare(left, "zh-CN"));

  for (const candidate of candidates) {
    if (await fileExists(candidate)) {
      return candidate;
    }
  }

  return "";
}

async function buildVisionRuntime(): Promise<{ client: OpenAI; config: ResolvedLlmRuntimeConfig } | null> {
  try {
    return await createLlmClient({ stage: "search_vision", maxRetries: 0 });
  } catch {
    return null;
  }
}

export function resolveSearchVisionModel() {
  return resolveStageLlmModel("search_vision");
}

function inferMimeType(filePath: string) {
  const extension = path.extname(filePath).toLowerCase();

  if (extension === ".png") return "image/png";
  if (extension === ".webp") return "image/webp";
  if (extension === ".gif") return "image/gif";
  if (extension === ".jpg" || extension === ".jpeg") return "image/jpeg";
  return "image/jpeg";
}

async function readImageAsDataUrl(filePath: string) {
  const buffer = await readFile(filePath);
  return `data:${inferMimeType(filePath)};base64,${buffer.toString("base64")}`;
}

function stringifyComponentRoles(value: unknown) {
  if (!value) {
    return "";
  }

  if (typeof value === "string") {
    return normalizeSpace(value);
  }

  if (typeof value === "object") {
    return Object.entries(value as Record<string, unknown>)
      .map(([key, item]) => `${key}:${ensureString(item)}`)
      .join("；");
  }

  return ensureString(value);
}

function inferMaterialId(report: ImageSearchReportInput) {
  const materialId = ensureString(report.material_id);
  if (materialId) {
    return materialId;
  }

  const match = ensureString(report.source_image_path).match(/material-([^/]+?)-/);
  if (match) {
    return match[1]!;
  }

  return "";
}

function isMeaningfulTerm(term: string) {
  if (term.length < 2 || term.length > 8) {
    return false;
  }

  if (/^\d+$/.test(term)) {
    return false;
  }

  return /[A-Za-z\u4e00-\u9fff]/.test(term);
}

function collectCandidateTerms(titles: string[]) {
  const counter = new Map<string, number>();

  for (const title of titles) {
    const normalized = title.replace(/\s+/g, "");
    const termsInTitle = new Set<string>();

    for (let start = 0; start < normalized.length; start += 1) {
      for (let length = 2; length <= 8 && start + length <= normalized.length; length += 1) {
        const term = normalized.slice(start, start + length);
        if (!isMeaningfulTerm(term)) {
          continue;
        }
        termsInTitle.add(term);
      }
    }

    for (const term of termsInTitle) {
      counter.set(term, (counter.get(term) ?? 0) + 1);
    }
  }

  return Array.from(counter.entries())
    .map(([term, hits]) => ({ term, hits }))
    .filter((item) => item.hits >= 2)
    .sort((left, right) => {
      if (right.hits !== left.hits) {
        return right.hits - left.hits;
      }
      if (right.term.length !== left.term.length) {
        return right.term.length - left.term.length;
      }
      return left.term.localeCompare(right.term, "zh-CN");
    });
}

function pruneRedundantTerms(terms: Array<{ term: string; hits: number }>) {
  const selected: Array<{ term: string; hits: number }> = [];

  for (const candidate of terms) {
    const shadowed = selected.some(
      (item) => item.hits === candidate.hits && item.term.includes(candidate.term)
    );
    if (!shadowed) {
      selected.push(candidate);
    }
  }

  return selected;
}

function buildStatsFromTerms(titles: string[], terms: string[]) {
  return terms
    .map((term) => {
      const hits = titles.filter((title) => title.includes(term)).length;
      return {
        term,
        hits,
        ratio: titles.length > 0 ? Number((hits / titles.length).toFixed(3)) : 0,
      };
    })
    .filter((item) => item.hits > 0)
    .sort((left, right) => {
      if (right.hits !== left.hits) {
        return right.hits - left.hits;
      }
      if (right.term.length !== left.term.length) {
        return right.term.length - left.term.length;
      }
      return left.term.localeCompare(right.term, "zh-CN");
    })
    .slice(0, 12);
}

function classifyTerm(term: string) {
  if (NOISE_HINTS.some((hint) => term.includes(hint))) {
    return "ignored" as const;
  }
  if (STRUCTURE_HINTS.some((hint) => term.includes(hint))) {
    return "structure" as const;
  }
  if (MATERIAL_HINTS.some((hint) => term.includes(hint))) {
    return "material" as const;
  }
  if (COMPONENT_HINTS.some((hint) => term.includes(hint))) {
    return "component" as const;
  }
  return "whole_product" as const;
}

function buildTitleAnalysis(titles: string[]): TitleAnalysis {
  const extractedTerms = pruneRedundantTerms(collectCandidateTerms(titles));
  const materialTerms = extractedTerms.filter((item) => classifyTerm(item.term) === "material").map((item) => item.term);
  const wholeProductTerms = extractedTerms.filter((item) => classifyTerm(item.term) === "whole_product").map((item) => item.term);
  const componentTerms = extractedTerms.filter((item) => classifyTerm(item.term) === "component").map((item) => item.term);
  const structureTerms = extractedTerms.filter((item) => classifyTerm(item.term) === "structure").map((item) => item.term);
  const ignoredTerms = extractedTerms.filter((item) => classifyTerm(item.term) === "ignored").map((item) => item.term);

  return {
    material_terms: buildStatsFromTerms(titles, materialTerms),
    whole_product_terms: buildStatsFromTerms(titles, wholeProductTerms),
    component_terms: buildStatsFromTerms(titles, componentTerms),
    structure_terms: buildStatsFromTerms(titles, structureTerms),
    ignored_terms: buildStatsFromTerms(titles, ignoredTerms),
  };
}

function findHintInText(text: string, hints: string[]) {
  const normalized = compactText(text);
  for (const hint of hints) {
    if (normalized.includes(compactText(hint))) {
      return hint;
    }
  }
  return "";
}

function pickMaterial(aiAnalysis: AiKeywordAnalysis | null, titleAnalysis: TitleAnalysis) {
  const aiText = [
    aiAnalysis?.suggested_keyword,
    aiAnalysis?.structure_summary,
    stringifyComponentRoles(aiAnalysis?.component_roles),
  ].join(" ");

  return findHintInText(aiText, MATERIAL_HINTS) || titleAnalysis.material_terms[0]?.term || "";
}

function pickSupplementaryMaterial(
  aiAnalysis: AiKeywordAnalysis | null,
  titleAnalysis: TitleAnalysis,
  primaryMaterial: string
) {
  const aiText = [
    aiAnalysis?.suggested_keyword,
    aiAnalysis?.structure_summary,
    stringifyComponentRoles(aiAnalysis?.component_roles),
  ].join(" ");

  const candidate = titleAnalysis.material_terms
    .map((item) => item.term)
    .find((term) => term && term !== primaryMaterial && (aiText.includes(term) || term === "玻璃"));

  return candidate ?? "";
}

function pickWholeProductTerm(aiAnalysis: AiKeywordAnalysis | null, titleAnalysis: TitleAnalysis) {
  const aiText = [aiAnalysis?.suggested_keyword, aiAnalysis?.structure_summary, aiAnalysis?.product_type].join(" ");
  const aiMatched = titleAnalysis.whole_product_terms.map((item) => item.term).filter((term) => aiText.includes(term));

  if (aiMatched.length > 0) {
    for (const stat of titleAnalysis.whole_product_terms) {
      if (aiMatched.includes(stat.term)) {
        return stat.term;
      }
    }

    return aiMatched[0]!;
  }

  return titleAnalysis.whole_product_terms[0]?.term ?? compactText(ensureString(aiAnalysis?.suggested_keyword)).slice(0, 8);
}

function pickCoreComponent(aiAnalysis: AiKeywordAnalysis | null, titleAnalysis: TitleAnalysis) {
  const aiText = [aiAnalysis?.structure_summary, stringifyComponentRoles(aiAnalysis?.component_roles)].join(" ");

  for (const term of titleAnalysis.component_terms.map((item) => item.term)) {
    if (aiText.includes(term)) {
      return term;
    }
  }

  return titleAnalysis.component_terms[0]?.term ?? "";
}

function pickStructureType(aiAnalysis: AiKeywordAnalysis | null, titleAnalysis: TitleAnalysis) {
  const aiText = [
    aiAnalysis?.suggested_keyword,
    aiAnalysis?.structure_summary,
    stringifyComponentRoles(aiAnalysis?.component_roles),
  ].join(" ");

  for (const term of titleAnalysis.structure_terms.map((item) => item.term)) {
    if (aiText.includes(term)) {
      return term;
    }
  }

  return titleAnalysis.structure_terms[0]?.term ?? "";
}

function compressMainKeyword(keyword: string) {
  const passes = [
    keyword,
    keyword.replace(/厂家|批发|定制|现货|直销/g, ""),
  ];

  for (const pass of passes) {
    const normalized = compactText(pass);
    if (normalized && normalized.length <= 20) {
      return normalized;
    }
  }

  return compactText(keyword).slice(0, 20);
}

function enforceMainKeyword(mainKeyword: string, aiAnalysis: AiKeywordAnalysis | null, titleAnalysis: TitleAnalysis) {
  const normalizedInput = compactText(mainKeyword);
  const primaryMaterial = pickMaterial(aiAnalysis, titleAnalysis);
  const supplementaryMaterial = pickSupplementaryMaterial(aiAnalysis, titleAnalysis, primaryMaterial);
  const wholeProductTerm = pickWholeProductTerm(aiAnalysis, titleAnalysis);
  const component = pickCoreComponent(aiAnalysis, titleAnalysis);
  const structureType = pickStructureType(aiAnalysis, titleAnalysis);

  if (normalizedInput) {
    const coversPrimaryMaterial = !primaryMaterial || normalizedInput.includes(compactText(primaryMaterial));
    const coversSupplementaryMaterial = !supplementaryMaterial || normalizedInput.includes(compactText(supplementaryMaterial));
    const coversComponent = !component || normalizedInput.includes(compactText(component));
    const coversWholeProduct = !wholeProductTerm || normalizedInput.includes(compactText(wholeProductTerm));

    if (coversPrimaryMaterial && coversSupplementaryMaterial && coversComponent && coversWholeProduct) {
      return compressMainKeyword(normalizedInput);
    }
  }

  return compressMainKeyword([
    primaryMaterial,
    component,
    supplementaryMaterial,
    wholeProductTerm,
    structureType,
  ].filter(Boolean).join(""));
}

function buildReasonText(imageBasis: string, titleAnalysisText: string, decisionLogic: string) {
  return [
    `1. 图片判断依据：${imageBasis}`,
    `2. 标题分析：${titleAnalysisText}`,
    `3. 最终决策逻辑：${decisionLogic}`,
  ].join("\n");
}

function buildFallbackResult(aiAnalysis: AiKeywordAnalysis | null, titleAnalysis: TitleAnalysis): FinalResult {
  const mainKeyword = enforceMainKeyword("", aiAnalysis, titleAnalysis);
  const adoptedMaterials = titleAnalysis.material_terms.slice(0, 3).map((item) => item.term).join("/") || "无";
  const adoptedWhole = titleAnalysis.whole_product_terms.slice(0, 3).map((item) => item.term).join("/") || "无";
  const ignoredComponents = titleAnalysis.component_terms.slice(0, 3).map((item) => item.term).join("/") || "无";
  const imageBasis = aiAnalysis
    ? `优先采用 ai_keyword_analysis 与图片结构结论：商品被识别为“${ensureString(aiAnalysis.product_type, "目标商品") }”，结构摘要为“${ensureString(aiAnalysis.structure_summary, "由多个有效部件组成的目标商品") }”。因此优先判断其是完整商品还是组合体，而不是直接被某个局部词带偏。`
    : "未取得 ai_keyword_analysis，只能依据图片路径与标题做弱判断。";
  const titleAnalysisText = `标题高频材质词为 ${adoptedMaterials}，高频产品词为 ${adoptedWhole}；${ignoredComponents} 被识别为组件验证词，只用于确认核心组件，不作为唯一主体。`;
  const decisionLogic = `严格按 ai_keyword_analysis > 图片 > 标题 的顺序收敛结果。主关键词优先保留完整市场叫法，不再为了压缩而拆词重组，因此输出 ${mainKeyword}。`;

  return {
    主关键词: mainKeyword,
    判断原因: buildReasonText(imageBasis, titleAnalysisText, decisionLogic),
  };
}

function parseArgs(argv: string[]) {
  const options = {
    inputPath: "",
    outputRoot: OUTPUT_ROOT,
  };

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

    if (flag === "input" && value) {
      options.inputPath = path.resolve(value);
    }

    if (flag === "output-root" && value) {
      options.outputRoot = path.resolve(value);
    }
  }

  return options;
}

function getPromptRuntimeScope(model = resolveSearchVisionModel()) {
  return {
    provider: process.env.PROMPT_PROVIDER || process.env.LLM_PROVIDER || "*",
    model: model || process.env.LLM_MODEL || "*",
  };
}

function summarizeTermStats(items: TermStat[]) {
  return items.map((item) => `${item.term}(${item.hits})`).join("，") || "无";
}

function buildPriorityFusionPromptVariables(aiAnalysis: AiKeywordAnalysis, titles: string[], titleAnalysis: TitleAnalysis) {
  return {
    material_terms_summary: summarizeTermStats(titleAnalysis.material_terms),
    whole_product_terms_summary: summarizeTermStats(titleAnalysis.whole_product_terms),
    component_terms_summary: summarizeTermStats(titleAnalysis.component_terms),
    structure_terms_summary: summarizeTermStats(titleAnalysis.structure_terms),
    titles_numbered: titles.map((title, index) => `${index + 1}. ${title}`).join("\n"),
    ai_suggested_keyword: ensureString(aiAnalysis.suggested_keyword),
    ai_product_type: ensureString(aiAnalysis.product_type),
    ai_structure_summary: ensureString(aiAnalysis.structure_summary),
    ai_component_roles: stringifyComponentRoles(aiAnalysis.component_roles),
  };
}

async function buildPriorityFusionPrompts(
  aiAnalysis: AiKeywordAnalysis,
  titles: string[],
  titleAnalysis: TitleAnalysis,
  model: string,
) {
  const promptBundle = await resolvePromptBundle(KEYWORD_FUSION_PROMPT_KEY, {
    expectedContentFormat: "multimodal_bundle",
    ...getPromptRuntimeScope(model),
  });
  const promptVariables = buildPriorityFusionPromptVariables(aiAnalysis, titles, titleAnalysis);
  const stepTemplates = getOptionalPayloadObject(promptBundle, "step_templates", promptBundle.promptKey);
  const renderedStepTemplates = {
    step1_image_analysis_prompt: renderPromptTemplate(
      getRequiredPayloadString(stepTemplates, "step1_image_analysis_prompt", `${promptBundle.promptKey}.step_templates`),
      promptVariables,
    ),
    step2_title_analysis_prompt: renderPromptTemplate(
      getRequiredPayloadString(stepTemplates, "step2_title_analysis_prompt", `${promptBundle.promptKey}.step_templates`),
      promptVariables,
    ),
    step3_fusion_prompt: renderPromptTemplate(
      getRequiredPayloadString(stepTemplates, "step3_fusion_prompt", `${promptBundle.promptKey}.step_templates`),
      promptVariables,
    ),
  };

  return {
    systemPrompt: getRequiredPayloadString(promptBundle, "system"),
    userPrompt: renderPromptTemplate(
      getRequiredPayloadString(promptBundle, "user_template"),
      {
        ...promptVariables,
        ...renderedStepTemplates,
      },
    ),
  };
}

async function analyzeWithPriorityFusion({
  client,
  aiAnalysis,
  model,
  sourceImagePath,
  screenshotPath,
  titles,
  titleAnalysis,
}: FusionAnalysisParams) {
  if (!(await fileExists(sourceImagePath))) {
    throw new Error(`源商品图片不存在: ${sourceImagePath}`);
  }

  if (!(await fileExists(screenshotPath))) {
    throw new Error(`搜索结果页截图不存在: ${screenshotPath}`);
  }

  const [sourceImageDataUrl, screenshotDataUrl] = await Promise.all([
    readImageAsDataUrl(sourceImagePath),
    readImageAsDataUrl(screenshotPath),
  ]);

  const { systemPrompt, userPrompt } = await buildPriorityFusionPrompts(aiAnalysis, titles, titleAnalysis, model);

  const response = await client.chat.completions.create({
    model,
    temperature: 0.1,
    response_format: { type: "json_object" },
    messages: [
      { role: "system", content: systemPrompt },
      {
        role: "user",
        content: [
          { type: "text", text: userPrompt },
          { type: "image_url", image_url: { url: sourceImageDataUrl } },
          { type: "image_url", image_url: { url: screenshotDataUrl } },
        ],
      },
    ],
  });

  const rawResponse = response.choices[0]?.message?.content ?? "";
  const parsed = tryParseJsonObject(rawResponse);
  if (!parsed) {
    throw new Error(`多模态返回不是有效 JSON: ${rawResponse}`);
  }

  return {
    rawResponse,
    parsed,
  };
}

function buildFinalResultFromModel(parsed: Record<string, unknown>, aiAnalysis: AiKeywordAnalysis | null, titleAnalysis: TitleAnalysis): FinalResult {
  const mainKeyword = enforceMainKeyword(ensureString(parsed.main_keyword), aiAnalysis, titleAnalysis);
  const imageBasis = ensureReadableText(
    parsed.image_basis,
    `图片与 ai_keyword_analysis 共同表明该对象更接近完整商品或组合体，而不是单独零件；可见核心组成部分与标题中的主体词能够相互印证。`
  );
  const titleAnalysisText = ensureReadableText(
    parsed.title_analysis,
    `标题高频材质词为 ${titleAnalysis.material_terms.slice(0, 3).map((item) => item.term).join("/") || "无"}，高频产品词为 ${titleAnalysis.whole_product_terms.slice(0, 3).map((item) => item.term).join("/") || "无"}；高频组件词为 ${titleAnalysis.component_terms.slice(0, 3).map((item) => item.term).join("/") || "无"}，组件词仅作验证，不作为唯一主体。`
  );
  const modelDecisionLogic = ensureReadableText(parsed.decision_logic);
  const decisionLogic = modelDecisionLogic.includes(mainKeyword)
    ? modelDecisionLogic
    : `优先依据 ai_keyword_analysis 和图片确认整体结构，再用标题验证市场叫法，所以主关键词采用 ${mainKeyword}。`;

  return {
    主关键词: mainKeyword,
    判断原因: buildReasonText(imageBasis, titleAnalysisText, decisionLogic),
  };
}

export async function runImageKeywordSelection(options: RunKeywordSelectionOptions = {}): Promise<RunKeywordSelectionResult> {
  const inputPath = options.inputPath || (await findLatestImageSearchReportPath());

  if (!inputPath) {
    throw new Error("未找到可用的 image-search-keyword-report.json，请通过 --input 显式传入");
  }

  const reportText = await readFile(inputPath, "utf8");
  const imageSearchReport = JSON.parse(reportText) as ImageSearchReportInput;
  const materialId = inferMaterialId(imageSearchReport);

  if (!materialId) {
    throw new Error("无法从输入报告解析 material_id");
  }

  const topTitles = Array.isArray(imageSearchReport.image_search?.top_titles)
    ? imageSearchReport.image_search.top_titles.map((item) => cleanTitle(item)).filter(Boolean)
    : [];
  const fallbackTitles = Array.isArray(imageSearchReport.similar_products)
    ? imageSearchReport.similar_products.map((item) => cleanTitle(item.title)).filter(Boolean)
    : [];
  const titles = dedupeList(topTitles.length > 0 ? topTitles : fallbackTitles).slice(0, 10);

  if (titles.length === 0) {
    throw new Error("输入报告中没有可用的 top_titles 或 similar_products.title");
  }

  const sourceImagePath = ensureString(imageSearchReport.source_image_path);
  const screenshotPath = ensureString(imageSearchReport.image_search?.screenshot_path);
  const keywordProcessPath = options.keywordProcessPath || (await findLatestKeywordProcessPath(materialId));

  if (!keywordProcessPath) {
    throw new Error(`未找到 material_id=${materialId} 的 keyword-process.json`);
  }

  const keywordProcessText = await readFile(keywordProcessPath, "utf8");
  const keywordProcess = JSON.parse(keywordProcessText) as KeywordProcessReport;
  const aiAnalysis = keywordProcess.ai_keyword_analysis ?? null;

  if (!aiAnalysis) {
    throw new Error(`keyword-process.json 缺少 ai_keyword_analysis: ${keywordProcessPath}`);
  }

  const titleAnalysis = buildTitleAnalysis(titles);
  const outputRoot = options.outputRoot ?? OUTPUT_ROOT;
  const createRunDir = options.createRunDir ?? true;
  const runDir = createRunDir ? path.join(outputRoot, `run-${nowStamp()}`) : outputRoot;
  const outputPath = path.join(runDir, options.resultFileName ?? "keyword-selection-test.json");
  const debugPath = path.join(runDir, options.debugFileName ?? "keyword-selection-debug.json");
  await mkdir(runDir, { recursive: true });

  const visionRuntime = await buildVisionRuntime();
  const searchVisionModel = visionRuntime?.config.model ?? resolveSearchVisionModel();
  const client = visionRuntime?.client ?? null;
  let isFallback = false;
  let fallbackReason = "";
  let multimodalRawResponse = "";
  let multimodalParsed: Record<string, unknown> | null = null;
  let finalResult: FinalResult;

  if (!client) {
    isFallback = true;
    fallbackReason = "缺少搜索视觉阶段 LLM Provider 配置，无法执行多模态判定";
    finalResult = buildFallbackResult(aiAnalysis, titleAnalysis);
  } else {
    try {
      const multimodal = await analyzeWithPriorityFusion({
        client,
        aiAnalysis,
        model: searchVisionModel,
        sourceImagePath,
        screenshotPath,
        titles,
        titleAnalysis,
      });
      multimodalRawResponse = multimodal.rawResponse;
      multimodalParsed = multimodal.parsed;
      finalResult = buildFinalResultFromModel(multimodal.parsed, aiAnalysis, titleAnalysis);
    } catch (error) {
      isFallback = true;
      fallbackReason = error instanceof Error ? error.message : String(error);
      finalResult = buildFallbackResult(aiAnalysis, titleAnalysis);
    }
  }

  const debugArchive: DebugArchive = {
    generated_at: new Date().toISOString(),
    input_report_path: inputPath,
    keyword_process_path: keywordProcessPath,
    material_id: materialId,
    source_image_path: sourceImagePath,
    screenshot_path: screenshotPath,
    top_titles: titles,
    ai_keyword_analysis: aiAnalysis,
    title_analysis: titleAnalysis,
    model_used: searchVisionModel,
    is_fallback: isFallback,
    fallback_reason: fallbackReason,
    multimodal_raw_response: multimodalRawResponse,
    multimodal_parsed: multimodalParsed,
    final_result: finalResult,
  };

  await writeFile(outputPath, JSON.stringify(finalResult, null, 2), "utf8");
  await writeFile(debugPath, JSON.stringify(debugArchive, null, 2), "utf8");

  return {
    runDir,
    outputPath,
    debugPath,
    materialId,
    inputPath,
    keywordProcessPath,
    isFallback,
    fallbackReason,
    finalResult,
  };
}

async function main() {
  const options = parseArgs(process.argv.slice(2));
  const result = await runImageKeywordSelection(options);

  console.log("\n════════════════ 关键词判定结果 ════════════════");
  console.log(`输入报告:   ${result.inputPath}`);
  console.log(`material_id: ${result.materialId}`);
  console.log(`Fallback:   ${result.isFallback ? `是 (${result.fallbackReason})` : "否"}`);
  console.log(`主关键词:   ${result.finalResult.主关键词}`);
  console.log("判断原因:");
  console.log(result.finalResult.判断原因);
  console.log(`结果文件:   ${result.outputPath}`);
  console.log(`调试文件:   ${result.debugPath}`);
  console.log("══════════════════════════════════════════════\n");
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  main().catch((error) => {
    console.error("\n❌ 图搜关键词判定失败:", error);
    process.exit(1);
  });
}
