/**
 * ============================================================
 * row-pipeline-sync.ts — 包装入口：row-pipeline + Supabase 同步
 * ============================================================
 * 用法：pnpm exec tsx automation/pipelines/row-pipeline-sync.ts --row=2
 *
 * 行为与 row-pipeline.ts 完全一致，
 * 但本地 output 仅作为临时中转目录：阶段完成后同步到 Supabase，
 * 每次运行前后都会自动清空，不保留截图和商家信息等本地产物。
 *
 * 断开方式：直接跑 row-pipeline.ts 即可，本文件是唯一碰 DB 的入口。
 * ============================================================
 */

import "dotenv/config";

import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import process from "node:process";
import { pathToFileURL } from "node:url";

import {
  parseArgs,
  applyMerchantLimit,
  stageKeywords,
  stageInitialSearch,
  stageImageSearch,
  readResultsFromPath,
  normalizeProductFromRaw,
  normalizeSearchPlanRoundFailureReason,
  type KeywordStageResult,
  type InitialSearchResult,
  type ImageSearchResult,
} from "./row-pipeline.ts";

import {
  upsertKeywordProcess,
  upsertSearchRound,
  upsertBestMatchImage,
  upsertExtendedBestMatches,
  upsertImageSearchResult,
  upsertFinalResult,
  upsertFinalResultProducts,
  upsertExtendedBestMatchRecords,
  updateSearchRoundStageDiagnostics,
  updateSearchRoundStageTiming,
  getSearchTaskById,
} from "../internal/db.ts";

import { uploadScreenshot } from "../internal/storage.ts";
import {
  looksLikeGeneratedResourceTitle,
  sanitizeSearchResultTitle,
} from "../playwright/search-result-extractor.js";

import type { SearchPipelineConfig } from "../core/types.ts";
import { DEFAULT_SEARCH_PIPELINE_CONFIG } from "../core/types.ts";
import type { SearchStageTiming, SearchStageTimingStatus, SearchStepTiming } from "../core/search-stage-timing.ts";
import {
  annotateStageProducts,
  buildPlaceholderStageProduct,
  buildKeywordArtifactPipelineConfig,
  buildMerchantKey,
  buildSearchExecutionStages,
  hasDetectedPhoneContact,
  mergeStageProducts,
  type SearchExecutionStage,
  type StageResultProduct,
} from "./multi-stage-results.ts";
import {
  buildScoredRoundCandidates,
  computeSemanticCandidateHitRate,
  mergeScoredCandidates,
  type FixedSearchRoundStatus,
  type ScoredSearchCandidate,
} from "./fixed-search-runtime.ts";
import { judgeCandidatesWithLlm, type LlmMatchedSearchCandidate } from "./fixed-search-llm.ts";
import {
  enrichSelectedOfferCandidates,
  type ContactEnrichmentResult,
  type EnrichedSelectedOffer,
} from "./offer-contact-enrichment.ts";
import { resolve1688ProfileDir } from "../playwright/profile-dir.js";

const AUTOMATION_DIR = path.resolve(import.meta.dirname!, "..");
const OUTPUT_ROOT = process.env.SEARCH_TASK_OUTPUT_ROOT
  ? path.resolve(process.env.SEARCH_TASK_OUTPUT_ROOT)
  : path.join(AUTOMATION_DIR, "output");
const ACTIVE_SEARCH_TASK_ID = String(process.env.SEARCH_TASK_ID ?? "").trim();
const uploadedFileCache = new Map<string, string>();
const SEARCH_CHILD_EVENT_PREFIX = "__SEARCH_EVENT__:";
const SEARCH_CHILD_ABORT_STATUSES = new Set(["completed", "failed", "paused", "stopped", "stopping"]);
const SEARCH_TASK_STATUS_CHECK_INTERVAL_MS = 3_000;
let lastSearchTaskStatusCheckAt = 0;
const DEFAULT_POSSIBLE_MERCHANT_LIMIT = 10;

type FinalResultPayload = {
  material_id: string;
  finished_at: string;
  source: string;
  main_keyword: string;
  product_count: number;
  products: unknown[];
};

type CompletedStageSnapshot = {
  label: string;
  source: string;
  mainKeyword: string;
  candidateCount: number;
  insertedCount: number;
  duplicateCount: number;
  keywordHitRate: number | null;
  stopReason: string;
  timing?: SearchStageTiming | null;
};

type SearchProgressMetrics = {
  candidateCount?: number;
  judgedCount?: number;
  nameMatchedCount?: number;
  attemptedContactCount?: number;
  qualifiedResultCount?: number;
  targetResultCount?: number | null;
};

type LatestKeywordStageContext = {
  search: InitialSearchResult;
  keywordResult: KeywordStageResult;
  stage: Extract<SearchExecutionStage, { kind: "keyword" }>;
};

type StageOutcome = {
  source: string;
  mainKeyword: string;
  products: ReturnType<typeof normalizeProductFromRaw>[];
  stageKeywordHitRate: number | null;
  stopPipeline: boolean;
  placeholderReason: string;
};

export function getKeywordStageSearchRoundIncrement(params: {
  keywordResult: { searchPlanRows?: unknown[] | null };
  initialRounds?: unknown[] | null;
}) {
  const searchPlanRowsCount = Array.isArray(params.keywordResult.searchPlanRows)
    ? params.keywordResult.searchPlanRows.length
    : 0;
  const initialRoundsCount = Array.isArray(params.initialRounds)
    ? params.initialRounds.length
    : 0;

  return Math.max(searchPlanRowsCount, initialRoundsCount, 1);
}

// ── sync helpers ─────────────────────────────────────────────

function syncLog(message: string) {
  console.log(`[sync] ${message}`);
}

function emitSearchProgress(params: {
  message: string;
  itemProgressPercent: number;
  softCapPercent?: number | null;
  stageStartedAt?: string;
  stageOrder?: number | null;
  stageLabel?: string;
  stageKind?: SearchExecutionStage["kind"] | string;
  stageStatus?: SearchStageTimingStatus;
  currentStep?: SearchStepTiming | null;
  completedStep?: SearchStepTiming | null;
  completedStage?: SearchStageTiming | null;
  metrics?: SearchProgressMetrics;
}) {
  const payload = {
    type: "progress_update",
    taskId: ACTIVE_SEARCH_TASK_ID || "",
    materialId: process.env.SEARCH_TASK_CURRENT_MATERIAL_ID || "",
    materialName: process.env.SEARCH_TASK_CURRENT_MATERIAL_NAME || "",
    message: params.message,
    itemProgressPercent: params.itemProgressPercent,
    softCapPercent: params.softCapPercent ?? null,
    stageStartedAt: params.stageStartedAt || new Date().toISOString(),
    stageOrder: params.stageOrder ?? null,
    stageLabel: params.stageLabel ?? "",
    stageKind: params.stageKind ?? "",
    stageStatus: params.stageStatus ?? "running",
    currentStep: params.currentStep ?? null,
    completedStep: params.completedStep ?? null,
    completedStage: params.completedStage ?? null,
    metrics: params.metrics,
  };
  console.log(`${SEARCH_CHILD_EVENT_PREFIX}${JSON.stringify(payload)}`);
}

function createStageTiming(params: {
  stage: SearchExecutionStage;
  materialId: string;
  startedAt: string;
  endedAt?: string;
  status?: SearchStageTimingStatus;
}): SearchStageTiming {
  const endedAt = params.endedAt ?? new Date().toISOString();
  const durationMs = Math.max(0, Date.parse(endedAt) - Date.parse(params.startedAt));
  return {
    taskId: ACTIVE_SEARCH_TASK_ID || "",
    materialId: params.materialId,
    materialName: process.env.SEARCH_TASK_CURRENT_MATERIAL_NAME || "",
    stageOrder: params.stage.stageOrder,
    stageLabel: params.stage.label,
    stageKind: params.stage.kind,
    startedAt: params.startedAt,
    endedAt,
    durationMs,
    elapsedMs: durationMs,
    status: params.status ?? "completed",
  };
}

function createStepTiming(params: {
  stage: SearchExecutionStage;
  materialId: string;
  stepKey: string;
  stepLabel: string;
  startedAt: string;
  endedAt?: string;
  status?: SearchStageTimingStatus;
}): SearchStepTiming {
  const endedAt = params.endedAt ?? new Date().toISOString();
  const durationMs = Math.max(0, Date.parse(endedAt) - Date.parse(params.startedAt));
  return {
    taskId: ACTIVE_SEARCH_TASK_ID || "",
    materialId: params.materialId,
    materialName: process.env.SEARCH_TASK_CURRENT_MATERIAL_NAME || "",
    stageOrder: params.stage.stageOrder,
    stageLabel: params.stage.label,
    stageKind: params.stage.kind,
    stepKey: params.stepKey,
    stepLabel: params.stepLabel,
    startedAt: params.startedAt,
    endedAt,
    durationMs,
    elapsedMs: durationMs,
    status: params.status ?? "completed",
  };
}

function emitStepStarted(params: {
  stage: SearchExecutionStage;
  materialId: string;
  stepKey: string;
  stepLabel: string;
  itemProgressPercent: number;
  softCapPercent?: number | null;
  startedAt?: string;
  metrics?: SearchProgressMetrics;
}) {
  const startedAt = params.startedAt ?? new Date().toISOString();
  const currentStep: SearchStepTiming = {
    taskId: ACTIVE_SEARCH_TASK_ID || "",
    materialId: params.materialId,
    materialName: process.env.SEARCH_TASK_CURRENT_MATERIAL_NAME || "",
    stageOrder: params.stage.stageOrder,
    stageLabel: params.stage.label,
    stageKind: params.stage.kind,
    stepKey: params.stepKey,
    stepLabel: params.stepLabel,
    startedAt,
    endedAt: null,
    durationMs: null,
    elapsedMs: 0,
    status: "running",
  };
  emitSearchProgress({
    message: params.stepLabel,
    itemProgressPercent: params.itemProgressPercent,
    softCapPercent: params.softCapPercent ?? null,
    stageStartedAt: startedAt,
    stageOrder: params.stage.stageOrder,
    stageLabel: params.stage.label,
    stageKind: params.stage.kind,
    stageStatus: "running",
    currentStep,
    metrics: params.metrics,
  });
  syncLog(`小阶段 ${params.stage.stageOrder}.${params.stepKey} ${params.stepLabel} 开始计时: ${startedAt}`);
  return currentStep;
}

function emitStepFinished(params: {
  stage: SearchExecutionStage;
  materialId: string;
  stepKey: string;
  stepLabel: string;
  startedAt: string;
  itemProgressPercent: number;
  softCapPercent?: number | null;
  status?: SearchStageTimingStatus;
  metrics?: SearchProgressMetrics;
}) {
  const completedStep = createStepTiming({
    stage: params.stage,
    materialId: params.materialId,
    stepKey: params.stepKey,
    stepLabel: params.stepLabel,
    startedAt: params.startedAt,
    status: params.status ?? "completed",
  });
  emitSearchProgress({
    message: `已完成${params.stepLabel}`,
    itemProgressPercent: params.itemProgressPercent,
    softCapPercent: params.softCapPercent ?? null,
    stageStartedAt: completedStep.startedAt,
    stageOrder: params.stage.stageOrder,
    stageLabel: params.stage.label,
    stageKind: params.stage.kind,
    stageStatus: params.status ?? "running",
    currentStep: completedStep,
    completedStep,
    metrics: params.metrics,
  });
  syncLog(`小阶段 ${params.stage.stageOrder}.${params.stepKey} ${params.stepLabel} 结束计时: ${completedStep.durationMs ?? 0}ms status=${completedStep.status}`);
  return completedStep;
}

function emitStageStarted(stage: SearchExecutionStage, materialId: string, startedAt: string) {
  emitSearchProgress({
    message: `正在执行${stage.label}`,
    itemProgressPercent: Math.max(5, Math.min(95, (stage.stageOrder - 1) * 20)),
    softCapPercent: null,
    stageStartedAt: startedAt,
    stageOrder: stage.stageOrder,
    stageLabel: stage.label,
    stageKind: stage.kind,
    stageStatus: "running",
  });
  syncLog(`阶段 ${stage.stageOrder} ${stage.label} 开始计时: ${startedAt}`);
}

function emitStageFinished(stage: SearchExecutionStage, timing: SearchStageTiming) {
  emitSearchProgress({
    message: `已完成${stage.label}`,
    itemProgressPercent: Math.max(10, Math.min(98, stage.stageOrder * 20)),
    softCapPercent: null,
    stageStartedAt: timing.startedAt,
    stageOrder: stage.stageOrder,
    stageLabel: stage.label,
    stageKind: stage.kind,
    stageStatus: timing.status,
    completedStage: timing,
  });
  syncLog(`阶段 ${stage.stageOrder} ${stage.label} 结束计时: ${timing.durationMs ?? 0}ms status=${timing.status}`);
}

async function assertActiveSearchTask(force = false) {
  if (!ACTIVE_SEARCH_TASK_ID) {
    return;
  }

  const now = Date.now();
  if (!force && now - lastSearchTaskStatusCheckAt < SEARCH_TASK_STATUS_CHECK_INTERVAL_MS) {
    return;
  }
  lastSearchTaskStatusCheckAt = now;

  const task = await getSearchTaskById(ACTIVE_SEARCH_TASK_ID);
  const status = String(task?.status ?? "");
  if (!task || SEARCH_CHILD_ABORT_STATUSES.has(status)) {
    const message = !task
      ? `搜索任务 ${ACTIVE_SEARCH_TASK_ID} 已不存在，停止子流程`
      : `搜索任务 ${ACTIVE_SEARCH_TASK_ID} 已标记为 ${status}，停止子流程`;
    const error = new Error(message);
    error.name = "SearchTaskStoppedError";
    throw error;
  }
}

function isSearchTaskStoppedError(error: unknown) {
  return error instanceof Error && error.name === "SearchTaskStoppedError";
}

async function assertActiveSearchTaskBestEffort() {
  try {
    await assertActiveSearchTask();
  } catch (error) {
    if (isSearchTaskStoppedError(error)) {
      throw error;
    }
    syncLog(`搜索任务状态检查失败，继续当前非关键审计阶段: ${error instanceof Error ? error.message : String(error)}`);
  }
}

async function trySync(label: string, fn: () => Promise<void>) {
  try {
    await fn();
    syncLog(`${label} ✓`);
  } catch (error) {
    const msg = error instanceof Error ? error.message : String(error);
    const stack = error instanceof Error ? error.stack : "";
    syncLog(`${label} ✗ (继续运行): ${msg}`);
    if (stack) {
      syncLog(`${label} Stack: ${stack}`);
    }
  }
}

async function criticalSync(label: string, fn: () => Promise<void>) {
  try {
    await fn();
    syncLog(`${label} ✓`);
  } catch (error) {
    const msg = error instanceof Error ? error.message : String(error);
    const stack = error instanceof Error ? error.stack : "";
    syncLog(`${label} ✗: ${msg}`);
    if (stack) {
      syncLog(`${label} Stack: ${stack}`);
    }
    throw error;
  }
}

export function buildSearchTaskStoragePrefix(materialId: string, taskId: string | null | undefined = ACTIVE_SEARCH_TASK_ID) {
  const normalizedTaskId = String(taskId ?? "").trim() || "legacy";
  return `search-task-${normalizedTaskId}/material-${materialId}`;
}

function materialPrefix(materialId: string) {
  return buildSearchTaskStoragePrefix(materialId);
}

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

function normalizeOfferUrlForLookup(value: unknown) {
  const raw = normalizeText(value);
  if (!raw) return "";

  try {
    const parsed = new URL(raw.startsWith("//") ? `https:${raw}` : raw);
    const offerIdMatch = parsed.pathname.match(/\/offer\/(\d+)\.html/i);
    const offerId = offerIdMatch?.[1] || parsed.searchParams.get("offerId") || parsed.searchParams.get("id") || "";
    if (offerId) {
      return `https://detail.1688.com/offer/${offerId}.html`;
    }
    parsed.search = "";
    parsed.hash = "";
    return parsed.toString();
  } catch {
    return raw.split(/[?#]/, 1)[0] ?? raw;
  }
}

export function isUnsafeProductTitle(value: unknown) {
  const title = normalizeText(value);
  if (!title) return true;
  if (looksLikeGeneratedResourceTitle(title)) return true;
  if (/^(?:https?:)?\/\//i.test(title)) return true;
  if (/^data:[a-z]+\/[a-z0-9.+-]+;base64,/i.test(title)) return true;
  return false;
}

function safeProductTitleCandidate(value: unknown) {
  if (isUnsafeProductTitle(value)) return "";
  const title = sanitizeSearchResultTitle(value);
  return title && !isUnsafeProductTitle(title) ? title : "";
}

function buildTitleFallbackLookup(products: Record<string, unknown>[] = []) {
  const lookup = new Map<string, string>();

  for (const product of products) {
    const offerUrl = normalizeOfferUrlForLookup(product.offer_url ?? product.offerUrl ?? product.url);
    if (!offerUrl || lookup.has(offerUrl)) continue;

    const sourceProduct = product.source_product && typeof product.source_product === "object"
      ? product.source_product as Record<string, unknown>
      : {};
    const title = safeProductTitleCandidate(product.title)
      || safeProductTitleCandidate(product["title-text"])
      || safeProductTitleCandidate(sourceProduct.title);

    if (title) {
      lookup.set(offerUrl, title);
    }
  }

  return lookup;
}

export function resolveFinalResultProductTitle(
  product: Record<string, unknown>,
  fallbackProducts: Record<string, unknown>[] = [],
) {
  return resolveFinalResultProductTitleFromLookup(product, buildTitleFallbackLookup(fallbackProducts));
}

function resolveFinalResultProductTitleFromLookup(
  product: Record<string, unknown>,
  fallbackLookup: Map<string, string>,
) {
  const sourceProduct = product.source_product && typeof product.source_product === "object"
    ? product.source_product as Record<string, unknown>
    : {};
  const directTitle = safeProductTitleCandidate(product.title);
  if (directTitle) return directTitle;

  const inlineFallback = safeProductTitleCandidate(product["title-text"])
    || safeProductTitleCandidate(sourceProduct.title);
  if (inlineFallback) return inlineFallback;

  const offerUrl = normalizeOfferUrlForLookup(product.offer_url ?? product.offerUrl ?? product.url);
  if (!offerUrl) return "";

  return fallbackLookup.get(offerUrl) ?? "";
}

function pickFirstNormalizedText(...values: unknown[]) {
  for (const value of values) {
    const normalized = normalizeText(value);
    if (normalized) {
      return normalized;
    }
  }

  return "";
}

async function cleanupTemporaryOutput() {
  syncLog(`已跳过本地临时 output 清理，保留排查产物: ${OUTPUT_ROOT}`);
}

export function buildFinalResultPayload(
  materialId: string,
  source: string,
  mainKeyword: string,
  products: unknown[],
): FinalResultPayload {
  return {
    material_id: materialId,
    finished_at: new Date().toISOString(),
    source,
    main_keyword: mainKeyword,
    product_count: products.length,
    products,
  };
}

export function dedupeFinalResultProductRowsByMerchant<T extends Record<string, unknown>>(rows: T[]): T[] {
  const seenKeys = new Set<string>();
  const deduped: T[] = [];

  for (const row of rows) {
    const merchantKey = buildMerchantKey({
      merchant_url: row.merchant_url ?? row.merchantUrl,
      contact_page_url: row.contact_page_url ?? row.contactPageUrl,
      merchant_name: row.merchant_name ?? row.merchantName,
      wangwang_url: row.wangwang_url ?? (row.contact as Record<string, unknown> | undefined)?.url,
      offer_url: row.offer_url ?? row.offerUrl ?? row.url,
      title: row.title ?? row["title-text"],
    });
    if (merchantKey && seenKeys.has(merchantKey)) {
      continue;
    }
    if (merchantKey) {
      seenKeys.add(merchantKey);
    }
    deduped.push(row);
  }

  return deduped;
}

function compareFixedRuntimeCandidateOrder(left: LlmMatchedSearchCandidate, right: LlmMatchedSearchCandidate) {
  return (
    right.score - left.score
    || left.search_round - right.search_round
    || left.url.localeCompare(right.url, "zh-CN")
  );
}

function getCandidateMerchantKey(candidate: LlmMatchedSearchCandidate) {
  return buildMerchantKey({
    merchant_url: candidate.source_product.merchant_url,
    contact_page_url: candidate.source_product.contact_page_url,
    merchant_name: candidate.source_product.merchant_name,
    wangwang_url: candidate.source_product.wangwang_url,
    offer_url: candidate.source_product.offer_url || candidate.url,
    title: candidate.source_product.title || candidate["title-text"],
  });
}

export function splitFixedRuntimeCandidatesByMatchBucket(candidates: LlmMatchedSearchCandidate[]) {
  const matchedCandidates = candidates
    .filter((candidate) => candidate.judge_status === "judged" && (candidate.match_bucket === "matched" || (candidate.match_bucket == null && candidate.is_match === true)))
    .sort(compareFixedRuntimeCandidateOrder);
  const matchedMerchantKeys = new Set(matchedCandidates.map(getCandidateMerchantKey).filter(Boolean));
  const possibleMerchantKeys = new Set<string>();
  const possibleCandidates: LlmMatchedSearchCandidate[] = [];

  for (const candidate of candidates
    .filter((entry) => entry.judge_status === "judged" && entry.match_bucket === "possible")
    .sort(compareFixedRuntimeCandidateOrder)) {
    const merchantKey = getCandidateMerchantKey(candidate);
    if (merchantKey && (matchedMerchantKeys.has(merchantKey) || possibleMerchantKeys.has(merchantKey))) {
      continue;
    }
    if (merchantKey) {
      possibleMerchantKeys.add(merchantKey);
    }
    possibleCandidates.push(candidate);
  }

  return { matchedCandidates, possibleCandidates };
}

export function resolvePossibleMerchantLimit(value: unknown = process.env.SEARCH_POSSIBLE_MERCHANT_LIMIT) {
  const normalized = String(value ?? "").trim();
  if (!normalized) {
    return DEFAULT_POSSIBLE_MERCHANT_LIMIT;
  }

  const parsed = Number.parseInt(normalized, 10);
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_POSSIBLE_MERCHANT_LIMIT;
}

async function tryUpload(filePath: string, storagePath: string): Promise<string> {
  const cached = uploadedFileCache.get(filePath);
  if (cached) {
    return cached;
  }

  try {
    const url = await uploadScreenshot(filePath, storagePath);
    uploadedFileCache.set(filePath, url);
    return url;
  } catch (error) {
    const msg = error instanceof Error ? error.message : String(error);
    syncLog(`上传失败 (${storagePath}): ${msg}`);
    return "";
  }
}

// ── 阶段同步函数 ────────────────────────────────────────────

async function syncKeywordProcess(materialId: string, batchId: string, keywordResult: KeywordStageResult) {
  await trySync("keyword_process", async () => {
    const report = JSON.parse(await readFile(keywordResult.keywordProcessPath, "utf8"));
    await upsertKeywordProcess({
      task_id: ACTIVE_SEARCH_TASK_ID || null,
      batch_id: batchId,
      material_id: materialId,
      row_name: String(report.row_name ?? ""),
      row_spec: String(report.row_spec ?? ""),
      row_brand: String(report.row_brand ?? ""),
      llm_model: String(report.llm_model ?? ""),
      ai_keyword_analysis: report.ai_keyword_analysis ?? {},
      full_report: report,
    });
  });
}

async function syncInitialSearch(
  materialId: string,
  batchId: string,
  search: InitialSearchResult,
  options: {
    searchRoundOffset?: number;
    stage?: Extract<SearchExecutionStage, { kind: "keyword" }>;
    stageTiming?: SearchStageTiming | null;
  } = {},
) {
  const prefix = materialPrefix(materialId);
  const searchRoundOffset = options.searchRoundOffset ?? 0;
  const stage = options.stage;

  // 同步每个搜索轮次
  for (const round of search.initialRounds) {
    const persistedSearchRound = searchRoundOffset + Number(round.search_round ?? 1);
    await trySync(`search_round initial #${persistedSearchRound}`, async () => {
      // 上传截图
      let screenshotUrl = "";
      const screenshotPath = String(round.page_screenshot_path ?? "");
      if (screenshotPath) {
        screenshotUrl = await tryUpload(
          screenshotPath,
          `${prefix}/initial-search/round-${persistedSearchRound}${path.extname(screenshotPath)}`,
        );
      }

      // 读取该轮商品结果
      let results: unknown[] = [];
      const resultsPath = String(round.results_path ?? "");
      if (resultsPath) {
        try {
          results = JSON.parse(await readFile(resultsPath, "utf8"));
          if (!Array.isArray(results)) results = [];
        } catch {
          results = [];
        }
      }

      await upsertSearchRound({
        task_id: ACTIVE_SEARCH_TASK_ID || null,
        batch_id: batchId,
        material_id: materialId,
        search_round: persistedSearchRound,
        run_type: "initial",
        primary_keyword: String(round.primary_keyword ?? ""),
        search_strategy: String(round.search_strategy ?? ""),
        stage_order: stage?.stageOrder ?? null,
        stage_label: stage?.label ?? "",
        stage_kind: stage?.kind ?? "keyword",
        stage_main_keyword: String(round.executed_keyword ?? round.primary_keyword ?? ""),
        brand_keyword: String(round.brand_keyword ?? ""),
        brand_keyword_hit_rate: round.brand_keyword_hit_rate != null ? Number(round.brand_keyword_hit_rate) : null,
        is_valid_search: Boolean(round.is_valid_search),
        visual_confidence: round.visual_confidence != null ? Number(round.visual_confidence) : null,
        visual_reason: String(round.visual_reason ?? ""),
        stop_reason: String(round.stop_reason ?? ""),
        results_count: Number(round.results_count ?? results.length ?? 0),
        results,
        screenshot_url: screenshotUrl,
        keyword_hit_rate: round.keyword_hit_rate != null ? Number(round.keyword_hit_rate) : null,
        source_candidate_count: round.source_candidate_count != null ? Number(round.source_candidate_count) : null,
        qualified_result_count: null,
        rejected_no_phone_count: null,
        target_result_count: null,
        shortage_reason: String(round.shortage_reason ?? ""),
        require_phone_contact: round.require_phone_contact == null ? true : Boolean(round.require_phone_contact),
        contact_enrichment_attempted: Boolean(round.contact_enrichment_attempted),
        contact_enrichment_skip_reason: String(round.contact_enrichment_skip_reason ?? ""),
        keyword_hit_rate_source: String(round.keyword_hit_rate_source ?? "local"),
        keyword_hit_rate_reason: String(round.keyword_hit_rate_reason ?? ""),
        keyword_hit_rate_sample_count: round.keyword_hit_rate_sample_count != null ? Number(round.keyword_hit_rate_sample_count) : null,
        stage_started_at: options.stageTiming?.startedAt ?? null,
        stage_ended_at: options.stageTiming?.endedAt ?? null,
        stage_duration_ms: options.stageTiming?.durationMs ?? null,
        stage_status: options.stageTiming?.status ?? "",
      });
    });
  }

  // 同步 best match image
  const bmi = search.bestMatchImage as Record<string, unknown> | null;
  if (bmi?.image_path) {
    await trySync("best_match_image", async () => {
      const imagePath = String(bmi.image_path);
      const imageUrl = await tryUpload(
        imagePath,
        `${prefix}/best-match${path.extname(imagePath)}`,
      );
      await upsertBestMatchImage({
        task_id: ACTIVE_SEARCH_TASK_ID || null,
        batch_id: batchId,
        material_id: materialId,
        image_url: imageUrl,
        offer_url: String(bmi.offer_url ?? ""),
        title: String(bmi.title ?? ""),
        merchant_name: String(bmi.merchant_name ?? ""),
        wangwang_url: String(bmi.wangwang_url ?? ""),
        selected_from_round: bmi.selected_from_round != null ? Number(bmi.selected_from_round) : null,
        selected_from_keyword: String(bmi.selected_from_keyword ?? ""),
        method: String(bmi.method ?? ""),
        reason: String(bmi.reason ?? ""),
      });
    });
  }

  // 同步 extended best matches
  await trySync("extended_best_matches", async () => {
    const report = await readExtendedBestMatchReport(search.searchRunDir);
    if (!report) {
      syncLog("extended_best_matches 跳过：未生成 extended-best-matches-report.json");
      return;
    }

    await upsertExtendedBestMatches({ task_id: ACTIVE_SEARCH_TASK_ID || null, batch_id: batchId, material_id: materialId, report });
  });

  // 同步 extended_best_match_records（展开每条记录）
  await trySync("extended_best_match_records", async () => {
    const report = await readExtendedBestMatchReport(search.searchRunDir);
    if (!report) {
      syncLog("extended_best_match_records 跳过：未生成 extended-best-matches-report.json");
      return;
    }

    const generatedAt = String(report.generated_at ?? new Date().toISOString());
    const records = Array.isArray(report.records) ? report.records : [];
    const rows = [];
    for (const r of records as Record<string, unknown>[]) {
      const screenshotLocalPath = String(r.offer_screenshot_path ?? "");
      let offerScreenshotUrl = "";
      if (screenshotLocalPath) {
        offerScreenshotUrl = await tryUpload(
          screenshotLocalPath,
          `${prefix}/offer-screenshots/extended-rank-${r.rank}.jpg`,
        );
      }
      rows.push({
        task_id: ACTIVE_SEARCH_TASK_ID || null,
        batch_id: batchId,
        material_id: materialId,
        generated_at: generatedAt,
        rank: Number(r.rank ?? 0),
        title: String(r.title ?? ""),
        offer_url: String(r.offer_url ?? ""),
        image_url: String(r.image_url ?? ""),
        merchant_name: String(r.merchant_name ?? ""),
        merchant_url: String(r.merchant_url ?? ""),
        wangwang_url: String(r.wangwang_url ?? ""),
        offer_screenshot_url: offerScreenshotUrl,
        score: r.score != null ? Number(r.score) : null,
        selected_from_round: r.selected_from_round != null ? Number(r.selected_from_round) : null,
        selected_from_keyword: String(r.selected_from_keyword ?? ""),
        detected_contacts: Array.isArray(r.detected_contacts) ? r.detected_contacts : [],
        contact_page_url: String(r.contact_page_url ?? ""),
      });
    }
    await upsertExtendedBestMatchRecords(materialId, rows, batchId, ACTIVE_SEARCH_TASK_ID);
  });
}

async function syncImageSearch(materialId: string, batchId: string, img: ImageSearchResult) {
  const prefix = materialPrefix(materialId);

  await trySync("image_search_result", async () => {
    const report = JSON.parse(await readFile(img.imageSearchReportPath, "utf8"));
    const imageSearch = report.image_search ?? {};

    let screenshotUrl = "";
    const screenshotPath = String(imageSearch.screenshot_path ?? "");
    if (screenshotPath) {
      screenshotUrl = await tryUpload(
        screenshotPath,
        `${prefix}/image-search/page${path.extname(screenshotPath)}`,
      );
    }

    await upsertImageSearchResult({
      task_id: ACTIVE_SEARCH_TASK_ID || null,
      batch_id: batchId,
      material_id: materialId,
      is_valid_image_search: img.isValidImageSearch,
      screenshot_url: screenshotUrl,
      top_titles: Array.isArray(imageSearch.top_titles) ? imageSearch.top_titles : [],
      similar_products: Array.isArray(report.similar_products) ? report.similar_products : [],
      text_rule: imageSearch.text_rule ?? {},
      image_similarity: imageSearch.image_similarity ?? {},
      full_report: report,
    });
  });
}

export async function syncFinalResult(
  materialId: string,
  batchId: string,
  data: FinalResultPayload,
  productRows?: Record<string, unknown>[],
  titleFallbackProducts: Record<string, unknown>[] = [],
) {
  await criticalSync("final_result", async () => {
    await upsertFinalResult({
      task_id: ACTIVE_SEARCH_TASK_ID || null,
      batch_id: batchId,
      material_id: materialId,
      source: String(data.source ?? ""),
      main_keyword: String(data.main_keyword ?? ""),
      product_count: Number(data.product_count ?? 0),
      products: Array.isArray(data.products) ? data.products : [],
      finished_at: String(data.finished_at ?? new Date().toISOString()),
    });
  });

  // 同步 final_result_products（展开每条商品）
  await criticalSync("final_result_products", async () => {
    const finishedAt = String(data.finished_at ?? new Date().toISOString());
    const products = Array.isArray(productRows)
      ? dedupeFinalResultProductRowsByMerchant(productRows)
      : (Array.isArray(data.products)
          ? dedupeFinalResultProductRowsByMerchant(data.products.filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object"))
          : []);
    const titleFallbackLookup = buildTitleFallbackLookup([
      ...titleFallbackProducts,
      ...(Array.isArray(data.products) ? data.products as Record<string, unknown>[] : []),
      ...(Array.isArray(productRows) ? productRows : []),
    ]);
    const rows = [];
    for (const [index, product] of (products as Record<string, unknown>[]).entries()) {
      const rank = Number(product.stage_rank ?? product.index ?? index + 1);
      const source = String(product.source ?? data.source ?? "");
      const mainKeyword = String(product.main_keyword ?? data.main_keyword ?? "");
      const screenshotLocalPath = String(product.offer_screenshot_path ?? "");
      const screenshotExt = path.extname(screenshotLocalPath) || ".jpg";
      const stageOrder = Number(product.stage_order ?? 0);
      let offerScreenshotUrl = "";
      if (screenshotLocalPath) {
        offerScreenshotUrl = await tryUpload(
          screenshotLocalPath,
          `${materialPrefix(materialId)}/offer-screenshots/${stageOrder > 0 ? `stage-${stageOrder}-rank-${rank}` : `final-rank-${rank}`}${screenshotExt}`,
        );
      }
      rows.push({
        task_id: ACTIVE_SEARCH_TASK_ID || null,
        batch_id: batchId,
        material_id: materialId,
        source,
        main_keyword: mainKeyword,
        stage_order: stageOrder > 0 ? stageOrder : null,
        stage_label: String(product.stage_label ?? ""),
        stage_kind: String(product.stage_kind ?? ""),
        stage_main_keyword: String(product.stage_main_keyword ?? mainKeyword),
        stage_keyword_hit_rate: product.stage_keyword_hit_rate != null ? Number(product.stage_keyword_hit_rate) : null,
        is_placeholder: Boolean(product.is_placeholder),
        placeholder_reason: String(product.placeholder_reason ?? ""),
        finished_at: finishedAt,
        rank,
        title: resolveFinalResultProductTitleFromLookup(product, titleFallbackLookup),
        offer_url: String(product.offer_url ?? ""),
        image_url: String(product.image_url ?? ""),
        merchant_name: String(product.merchant_name ?? ""),
        merchant_url: String(product.merchant_url ?? ""),
        wangwang_url: String(product.wangwang_url ?? ""),
        offer_screenshot_url: offerScreenshotUrl,
        contact_page_url: String(product.contact_page_url ?? ""),
        detected_contacts: Array.isArray(product.detected_contacts) ? product.detected_contacts : [],
        score: product.score != null ? Number(product.score) : null,
        is_match: product.is_match == null ? null : Boolean(product.is_match),
        match_bucket: String(product.match_bucket ?? (product.is_match === false ? "not_match" : "matched")),
        reason: String(product.reason ?? ""),
        has_phone_contact: Boolean(product.has_phone_contact),
        qualification_status: String(product.qualification_status ?? (product.is_placeholder ? "placeholder" : "qualified")),
        contact_collection_reason: String(product.contact_collection_reason ?? ""),
      });
    }
    await upsertFinalResultProducts(materialId, rows, batchId, ACTIVE_SEARCH_TASK_ID);
  });
}

// ── main ─────────────────────────────────────────────────────

function parsePipelineConfig(): SearchPipelineConfig {
  const raw = process.env.SEARCH_PIPELINE_CONFIG;
  if (!raw) return DEFAULT_SEARCH_PIPELINE_CONFIG;
  try {
    return JSON.parse(raw) as SearchPipelineConfig;
  } catch {
    syncLog(`SEARCH_PIPELINE_CONFIG 解析失败，使用默认配置`);
    return DEFAULT_SEARCH_PIPELINE_CONFIG;
  }
}

function buildFallbackMainKeyword(search: InitialSearchResult) {
  return String(
    search.bestMatchImage?.selected_from_keyword
      ?? search.validRound?.primary_keyword
      ?? search.initialRounds[0]?.primary_keyword
      ?? "",
  );
}

async function readExtendedBestMatchReport(searchRunDir: string): Promise<Record<string, unknown> | null> {
  const extendedReportPath = path.join(searchRunDir, "extended-best-matches", "extended-best-matches-report.json");
  try {
    return JSON.parse(await readFile(extendedReportPath, "utf8")) as Record<string, unknown>;
  } catch {
    return null;
  }
}

async function readExtendedBestMatchProducts(searchRunDir: string, limit = 0) {
  const extendedReport = await readExtendedBestMatchReport(searchRunDir);
  if (!extendedReport) {
    return [];
  }

  return applyMerchantLimit(
    Array.isArray(extendedReport.records) ? extendedReport.records : [],
    limit,
  ).map((item, index) => normalizeProductFromRaw(item as Record<string, unknown>, index + 1));
}

async function readImageSearchProducts(imageSearchReportPath: string, limit = 0) {
  try {
    const imageSearchReport = JSON.parse(await readFile(imageSearchReportPath, "utf8"));
    return applyMerchantLimit(
      Array.isArray(imageSearchReport.similar_products) ? imageSearchReport.similar_products : [],
      limit,
    ).map((item, index) => normalizeProductFromRaw(item as Record<string, unknown>, index + 1));
  } catch {
    return [];
  }
}

type FixedRuntimeSelectionResult = {
  source: string;
  mainKeyword: string;
  candidateCount: number;
  duplicateCount: number;
  hitRate: number | null;
  llmMatchedCount: number;
  attemptedContactCount: number;
  qualifiedContactCount: number;
  targetResultCount: number | null;
  shortageReason: string;
  artifacts: {
    llmMatchResultsPath: string;
    contactEnrichmentResultsPath: string;
    possibleContactEnrichmentResultsPath?: string;
    finalSelectedProductsPath: string;
    diagnosticsSummaryPath: string;
  };
  finalProducts: Array<Record<string, unknown>>;
};

function buildEmptyPossibleContactEnrichmentResult(params: {
  targetQualifiedCount: number;
  shortageReason: string;
  errorMessage?: string;
}): ContactEnrichmentResult {
  return {
    enriched: [],
    attempts: [],
    attemptedCount: 0,
    attemptedCandidateCount: 0,
    qualifiedCount: 0,
    targetQualifiedCount: params.targetQualifiedCount,
    initialWindowSize: 0,
    duplicateMerchantCount: 0,
    shortageReason: params.shortageReason,
    ...(params.errorMessage ? { errorMessage: params.errorMessage } : {}),
  };
}

async function enrichPossibleContactCandidates(params: {
  candidates: LlmMatchedSearchCandidate[];
  screenshotDir: string;
  headless: boolean;
  profileDir: string;
  targetQualifiedCount: number;
  auditPath: string;
}) {
  if (params.candidates.length === 0 || params.targetQualifiedCount === 0) {
    const result = buildEmptyPossibleContactEnrichmentResult({
      targetQualifiedCount: params.targetQualifiedCount,
      shortageReason: params.candidates.length === 0
        ? "no_possible_candidates"
        : "possible_merchant_limit_zero",
    });
    await writeFile(params.auditPath, JSON.stringify(result, null, 2), "utf8");
    return result;
  }

  try {
    return await enrichSelectedOfferCandidates({
      candidates: params.candidates,
      screenshotDir: params.screenshotDir,
      headless: params.headless,
      profileDir: params.profileDir,
      requireLoggedIn: true,
      targetQualifiedCount: params.targetQualifiedCount,
      auditPath: params.auditPath,
      onCandidateAttempt: async ({ attemptedCount, qualifiedCount, targetQualifiedCount }) => {
        await assertActiveSearchTaskBestEffort();
        syncLog(`可能商家联系方式补齐: 已尝试=${attemptedCount} 合格=${qualifiedCount}/${targetQualifiedCount ?? params.candidates.length}`);
      },
    });
  } catch (error) {
    if (isSearchTaskStoppedError(error)) {
      throw error;
    }
    const message = error instanceof Error ? error.message : String(error);
    syncLog(`可能商家联系方式补齐失败，保留已获得商家继续收尾: ${message}`);
    const result = buildEmptyPossibleContactEnrichmentResult({
      targetQualifiedCount: params.targetQualifiedCount,
      shortageReason: "possible_contact_enrichment_failed",
      errorMessage: message,
    });
    await writeFile(params.auditPath, JSON.stringify(result, null, 2), "utf8").catch((writeError) => {
      syncLog(`possible_contact_enrichment_failed 审计写入失败: ${writeError instanceof Error ? writeError.message : String(writeError)}`);
    });
    return result;
  }
}

function incrementCount(target: Record<string, number>, key: string) {
  const normalized = normalizeText(key) || "unknown";
  target[normalized] = (target[normalized] ?? 0) + 1;
}

export function buildFixedRuntimeDiagnosticsSummary(params: {
  materialId: string;
  batchId: string;
  shortageReason: string;
  targetQualifiedCount: number | null | undefined;
  llmMatchedCount: number;
  llmPossibleCount?: number;
  mergedCandidateCount: number;
  qualifiedContactCount: number;
  attemptedContactCount: number;
  contactEnrichmentResult: ContactEnrichmentResult;
  possibleContactEnrichmentResult?: ContactEnrichmentResult;
  llmCandidates?: Array<Record<string, unknown>>;
  finalProducts?: Array<Record<string, unknown>>;
  artifacts: {
    llmMatchResultsPath: string;
    contactEnrichmentResultsPath: string;
    possibleContactEnrichmentResultsPath?: string;
    finalSelectedProductsPath: string;
  };
}) {
  const reasonCounts: Record<string, number> = {};
  const offerScreenshotStatusCounts: Record<string, number> = {};
  const contactFetchStatusCounts: Record<string, number> = {};

  const allContactAttempts = [
    ...params.contactEnrichmentResult.attempts,
    ...(params.possibleContactEnrichmentResult?.attempts ?? []),
  ];
  const candidateAttempts = allContactAttempts.map((attempt) => {
    const failureReason = attempt.qualification_status === "qualified"
      ? "qualified"
      : normalizeText(attempt.contact_collection_reason) || normalizeText(attempt.error_message) || "unknown";
    incrementCount(reasonCounts, failureReason);
    incrementCount(offerScreenshotStatusCounts, attempt.offerScreenshotStatus);
    incrementCount(contactFetchStatusCounts, attempt.contactFetchStatus);

    return {
      candidate_index: attempt.candidate_index,
      phase: attempt.phase,
      title: attempt["title-text"],
      url: attempt.url,
      score: attempt.score,
      search_round: attempt.search_round,
      round_label: attempt.round_label,
      qualification_status: attempt.qualification_status,
      failure_reason: failureReason,
      offerScreenshotStatus: attempt.offerScreenshotStatus,
      contactFetchStatus: attempt.contactFetchStatus,
      contactFetchMethod: attempt.contactFetchMethod,
      contact_page_url: attempt.contact_page_url,
      contact_count: Array.isArray(attempt.contact_page_contacts) ? attempt.contact_page_contacts.length : 0,
      has_phone_contact: attempt.has_phone_contact,
      error_message: attempt.error_message,
    };
  });
  const llmCandidates = Array.isArray(params.llmCandidates) ? params.llmCandidates : [];
  const finalProducts = Array.isArray(params.finalProducts) ? params.finalProducts : [];
  const llmMatchSummary = {
    total: llmCandidates.length,
    judged: llmCandidates.filter((candidate) => normalizeText(candidate.judge_status) === "judged").length,
    matched: llmCandidates.filter((candidate) => normalizeText(candidate.match_bucket) === "matched" || candidate.is_match === true).length,
    possible: llmCandidates.filter((candidate) => normalizeText(candidate.match_bucket) === "possible").length,
    llm_error: llmCandidates.filter((candidate) => normalizeText(candidate.judge_status) === "llm_error").length,
    top_matches: llmCandidates
      .filter((candidate) => normalizeText(candidate.match_bucket) === "matched" || candidate.is_match === true)
      .slice(0, 20)
      .map((candidate) => ({
        title: normalizeText(candidate["title-text"] ?? candidate.title),
        url: normalizeText(candidate.url),
        score: Number(candidate.score ?? 0),
        confidence: Number(candidate.confidence ?? 0),
        reason: normalizeText(candidate.reason),
        match_bucket: normalizeText(candidate.match_bucket),
        search_round: Number(candidate.search_round ?? 0),
        round_label: normalizeText(candidate.round_label),
      })),
  };
  const finalSelectedSummary = {
    count: finalProducts.length,
    products: finalProducts.slice(0, 20).map((product) => ({
      title: normalizeText(product.title ?? product["title-text"]),
      offer_url: normalizeText(product.offer_url ?? product.url),
      merchant_name: normalizeText(product.merchant_name),
      merchant_url: normalizeText(product.merchant_url),
      contact_page_url: normalizeText(product.contact_page_url),
      match_bucket: normalizeText(product.match_bucket) || "matched",
      has_phone_contact: Boolean(product.has_phone_contact),
    })),
  };

  return {
    generated_at: new Date().toISOString(),
    material_id: params.materialId,
    batch_id: params.batchId,
    shortage_reason: params.shortageReason,
    target_qualified_count: params.targetQualifiedCount ?? null,
    llm_matched_count: params.llmMatchedCount,
    llm_possible_count: params.llmPossibleCount ?? 0,
    merged_candidate_count: params.mergedCandidateCount,
    attempted_contact_count: params.attemptedContactCount,
    qualified_contact_count: params.qualifiedContactCount,
    duplicate_merchant_count: params.contactEnrichmentResult.duplicateMerchantCount,
    initial_window_size: params.contactEnrichmentResult.initialWindowSize,
    reason_counts: reasonCounts,
    offer_screenshot_status_counts: offerScreenshotStatusCounts,
    contact_fetch_status_counts: contactFetchStatusCounts,
    llm_match_summary: llmMatchSummary,
    final_selected_summary: finalSelectedSummary,
    candidate_attempts: candidateAttempts,
    artifacts: params.artifacts,
  };
}

function buildFixedRuntimeMainKeyword(roundStatuses: FixedSearchRoundStatus[]) {
  return roundStatuses
    .filter((status) => status.status === "success")
    .map((status) => status.query)
    .filter(Boolean)
    .join(" | ");
}

function formatNullableHitRate(value: number | null) {
  return value == null ? "-" : `${(value * 100).toFixed(1)}%`;
}

async function loadFixedSearchRoundStatuses(keywordResult: KeywordStageResult): Promise<FixedSearchRoundStatus[]> {
  if (Array.isArray((keywordResult as { searchRoundStatuses?: unknown[] }).searchRoundStatuses)) {
    return ((keywordResult as { searchRoundStatuses?: unknown[] }).searchRoundStatuses ?? [])
      .filter((item): item is FixedSearchRoundStatus => Boolean(item && typeof item === "object"));
  }

  const searchRoundStatusPath = String((keywordResult as { searchRoundStatusPath?: unknown }).searchRoundStatusPath ?? "");
  if (!searchRoundStatusPath) {
    return [];
  }

  try {
    const parsed = JSON.parse(await readFile(searchRoundStatusPath, "utf8"));
    return Array.isArray(parsed)
      ? parsed.filter((item): item is FixedSearchRoundStatus => Boolean(item && typeof item === "object"))
      : [];
  } catch {
    return [];
  }
}

async function readRawRoundProducts(round: Record<string, unknown> | null | undefined) {
  const resultsPath = String(round?.results_path ?? "");
  if (resultsPath) {
    try {
      const parsed = JSON.parse(await readFile(resultsPath, "utf8"));
      return Array.isArray(parsed) ? parsed as Record<string, unknown>[] : [];
    } catch {
      return [];
    }
  }

  return Array.isArray(round?.results) ? round?.results as Record<string, unknown>[] : [];
}

async function readSearchRoundTitleFallbackProducts(rounds: Record<string, unknown>[] = []) {
  const products: Record<string, unknown>[] = [];
  for (const round of rounds) {
    products.push(...await readRawRoundProducts(round));
  }
  return products;
}

export function resolveFixedRuntimeSearchBlockingReason(rounds: Record<string, unknown>[] = []) {
  const relevantRounds = rounds.filter((round) => {
    const hasRoundIdentity = normalizeText(round.search_round)
      || normalizeText(round.primary_keyword)
      || normalizeText(round.executed_keyword);
    return Boolean(hasRoundIdentity);
  });
  if (relevantRounds.length === 0) {
    return "";
  }

  const hasActualProducts = relevantRounds.some((round) => {
    if (normalizeText(round.results_path)) {
      return true;
    }
    if (Array.isArray(round.results) && round.results.length > 0) {
      return true;
    }
    return false;
  });
  if (hasActualProducts) {
    return "";
  }

  const reasons = relevantRounds
    .map((round) => normalizeSearchPlanRoundFailureReason(
      round.round_failure_reason ?? round.visual_reason ?? round.keyword_hit_rate_reason,
    ))
    .filter(Boolean);
  if (reasons.length !== relevantRounds.length) {
    return "";
  }

  if (reasons.every((reason) => reason === "login_intercept")) {
    return "login_intercept";
  }
  if (reasons.every((reason) => reason === "captcha_intercept")) {
    return "captcha_intercept";
  }
  if (reasons.every((reason) => reason === "browser_target_crashed")) {
    return "browser_target_crashed";
  }
  if (reasons.every((reason) => reason === "login_intercept" || reason === "captcha_intercept")) {
    return reasons.includes("login_intercept") ? "login_intercept" : "captcha_intercept";
  }

  return "";
}

export function shouldFailFixedRuntimeSearchBlockingReason(reason: string) {
  return reason === "browser_target_crashed";
}

export function buildFixedRuntimeLlmContext(keywordResult: KeywordStageResult, roundStatuses: FixedSearchRoundStatus[]) {
  const targetRow = keywordResult.targetRow as Record<string, unknown> | undefined;
  return {
    primary_search_term: pickFirstNormalizedText(
      keywordResult.aiKeywordAnalysis?.suggested_keyword,
      buildFixedRuntimeMainKeyword(roundStatuses).split(" | ")[0],
    ),
    standardized_name: pickFirstNormalizedText(targetRow?.名称),
    brand: pickFirstNormalizedText(keywordResult.analysis?.brand_term, targetRow?.品牌),
    standardized_spec: pickFirstNormalizedText(keywordResult.analysis?.simplified_spec, targetRow?.规格型号),
    category: pickFirstNormalizedText(keywordResult.analysis?.material),
    ai_reasoning: pickFirstNormalizedText(keywordResult.aiKeywordAnalysis?.structure_summary),
    product_type: pickFirstNormalizedText(keywordResult.aiKeywordAnalysis?.product_type),
  };
}

async function resolveFixedRuntimeSelection(params: {
  materialId: string;
  batchId: string;
  search: InitialSearchResult;
  keywordResult: KeywordStageResult;
  stage: Extract<SearchExecutionStage, { kind: "keyword" }>;
  contactLimit: number;
  headless: boolean;
}) : Promise<FixedRuntimeSelectionResult> {
  await assertActiveSearchTask(true);
  const candidateMergeStepStartedAt = emitStepStarted({
    stage: params.stage,
    materialId: params.materialId,
    stepKey: "candidate_merge",
    stepLabel: "搜索候选汇总去重",
    itemProgressPercent: 35,
    softCapPercent: 45,
    metrics: {
      candidateCount: 0,
      judgedCount: 0,
      nameMatchedCount: 0,
      attemptedContactCount: 0,
      qualifiedResultCount: 0,
      targetResultCount: Number.isInteger(params.contactLimit) && params.contactLimit > 0 ? params.contactLimit : null,
    },
  }).startedAt;
  const roundStatuses = await loadFixedSearchRoundStatuses(params.keywordResult);
  const successfulRoundStatuses = roundStatuses.filter((status) => status.status === "success");
  const roundMap = new Map(
    params.search.initialRounds.map((round) => [Number(round.search_round ?? 0), round as Record<string, unknown>]),
  );
  const roundCandidates: ScoredSearchCandidate[] = [];

  for (const roundStatus of successfulRoundStatuses) {
    const rawProducts = await readRawRoundProducts(roundMap.get(roundStatus.round));
    roundCandidates.push(
      ...buildScoredRoundCandidates({
        round: roundStatus,
        products: rawProducts,
      }),
    );
  }

  const mergedCandidates = mergeScoredCandidates(roundCandidates);
  const llmMatchResultsPath = path.join(params.search.searchRunDir, "llm-match-results.json");
  const contactAuditPath = path.join(params.search.searchRunDir, "contact-enrichment-results.json");
  const possibleContactAuditPath = path.join(params.search.searchRunDir, "possible-contact-enrichment-results.json");
  const finalSelectedProductsPath = path.join(params.search.searchRunDir, "final-selected-products.json");
  const targetQualifiedCount = Number.isInteger(params.contactLimit) && params.contactLimit > 0
    ? params.contactLimit
    : undefined;
  const possibleTargetQualifiedCount = resolvePossibleMerchantLimit();
  const searchBlockingReason = resolveFixedRuntimeSearchBlockingReason(params.search.initialRounds as Record<string, unknown>[]);
  emitStepFinished({
    stage: params.stage,
    materialId: params.materialId,
    stepKey: "candidate_merge",
    stepLabel: "搜索候选汇总去重",
    startedAt: candidateMergeStepStartedAt,
    itemProgressPercent: 42,
    softCapPercent: 45,
    metrics: {
      candidateCount: mergedCandidates.length,
      judgedCount: 0,
      nameMatchedCount: 0,
      attemptedContactCount: 0,
      qualifiedResultCount: 0,
      targetResultCount: Number.isInteger(params.contactLimit) && params.contactLimit > 0 ? params.contactLimit : null,
    },
  });
  if (searchBlockingReason && mergedCandidates.length === 0) {
    const contactEnrichmentResult: ContactEnrichmentResult = {
      enriched: [],
      attempts: [],
      attemptedCount: 0,
      attemptedCandidateCount: 0,
      qualifiedCount: 0,
      targetQualifiedCount: targetQualifiedCount ?? null,
      initialWindowSize: 0,
      duplicateMerchantCount: 0,
      shortageReason: searchBlockingReason,
    };
    const finalProducts: Array<Record<string, unknown>> = [];
    await writeFile(llmMatchResultsPath, JSON.stringify([], null, 2), "utf8");
    await writeFile(contactAuditPath, JSON.stringify(contactEnrichmentResult, null, 2), "utf8");
    await writeFile(possibleContactAuditPath, JSON.stringify(contactEnrichmentResult, null, 2), "utf8");
    await writeFile(finalSelectedProductsPath, JSON.stringify(finalProducts, null, 2), "utf8");
    const fixedRuntimeDiagnosticsDir = path.join(OUTPUT_ROOT, "fixed-runtime-diagnostics", ACTIVE_SEARCH_TASK_ID || "manual-run");
    await mkdir(fixedRuntimeDiagnosticsDir, { recursive: true });
    const diagnosticsSummaryPath = path.join(
      fixedRuntimeDiagnosticsDir,
      `${params.batchId}-${params.materialId}-fixed-runtime-diagnostics.json`,
    );
    await writeFile(
      diagnosticsSummaryPath,
      JSON.stringify(buildFixedRuntimeDiagnosticsSummary({
        materialId: params.materialId,
        batchId: params.batchId,
        shortageReason: searchBlockingReason,
        targetQualifiedCount,
        llmMatchedCount: 0,
        mergedCandidateCount: 0,
        qualifiedContactCount: 0,
        attemptedContactCount: 0,
        contactEnrichmentResult,
        llmCandidates: [],
        finalProducts,
        artifacts: {
          llmMatchResultsPath,
          contactEnrichmentResultsPath: contactAuditPath,
          possibleContactEnrichmentResultsPath: possibleContactAuditPath,
          finalSelectedProductsPath,
        },
      }), null, 2),
      "utf8",
    );
    await trySync("fixed_runtime_search_blocked_diagnostics", async () => {
      await updateSearchRoundStageDiagnostics({
        batchId: params.batchId,
        materialId: params.materialId,
        stageOrder: params.stage.stageOrder,
        taskId: ACTIVE_SEARCH_TASK_ID || null,
        sourceCandidateCount: 0,
        keywordHitRate: null,
        keywordHitRateSource: "search_blocked",
        keywordHitRateReason: searchBlockingReason,
        keywordHitRateSampleCount: 0,
        targetResultCount: targetQualifiedCount ?? null,
        shortageReason: searchBlockingReason,
        contactEnrichmentAttempted: false,
        contactEnrichmentSkipReason: searchBlockingReason,
      });
    });
    syncLog(`固定运行时搜索阶段被拦截: material=${params.materialId} reason=${searchBlockingReason}`);
    if (shouldFailFixedRuntimeSearchBlockingReason(searchBlockingReason)) {
      throw new Error("搜索浏览器崩溃，未进入结果页");
    }
    return {
      source: "fixed_runtime_search",
      mainKeyword: buildFixedRuntimeMainKeyword(roundStatuses),
      candidateCount: 0,
      duplicateCount: 0,
      hitRate: null,
      llmMatchedCount: 0,
      attemptedContactCount: 0,
      qualifiedContactCount: 0,
      targetResultCount: targetQualifiedCount ?? null,
      shortageReason: searchBlockingReason,
      artifacts: {
        llmMatchResultsPath,
        contactEnrichmentResultsPath: contactAuditPath,
        possibleContactEnrichmentResultsPath: possibleContactAuditPath,
        finalSelectedProductsPath,
        diagnosticsSummaryPath,
      },
      finalProducts,
    };
  }
  emitSearchProgress({
    message: `已完成搜索结果汇总，正在准备大模型商品匹配判断...`,
    itemProgressPercent: 35,
    softCapPercent: 45,
    metrics: {
      candidateCount: mergedCandidates.length,
      judgedCount: 0,
      nameMatchedCount: 0,
      attemptedContactCount: 0,
      qualifiedResultCount: 0,
      targetResultCount: Number.isInteger(params.contactLimit) && params.contactLimit > 0 ? params.contactLimit : null,
    },
  });
  const llmMatchStepStartedAt = emitStepStarted({
    stage: params.stage,
    materialId: params.materialId,
    stepKey: "llm_match",
    stepLabel: "LLM 商品匹配候选筛选",
    itemProgressPercent: 45,
    softCapPercent: 82,
  }).startedAt;
  const llmStageStartedAt = new Date().toISOString();
  emitSearchProgress({
    message: "正在调用大模型判断商品匹配，请稍候...",
    itemProgressPercent: 45,
    softCapPercent: 75,
    stageStartedAt: llmStageStartedAt,
    metrics: {
      candidateCount: mergedCandidates.length,
      judgedCount: 0,
      nameMatchedCount: 0,
    },
  });
  const llmMatchedCandidates = await judgeCandidatesWithLlm({
    context: buildFixedRuntimeLlmContext(params.keywordResult, roundStatuses),
    candidates: mergedCandidates,
    taskId: ACTIVE_SEARCH_TASK_ID || "",
    materialId: params.materialId,
    outputDir: params.search.searchRunDir,
    onProgress: async ({ completedChunks, totalChunks, processedCandidates, totalCandidates, provider, model, message }) => {
      await assertActiveSearchTask();
      const progress = 55 + Math.round((completedChunks / Math.max(1, totalChunks)) * 25);
      emitSearchProgress({
        message: message ?? `正在等待 ${provider}/${model} 在线商品匹配结果: ${completedChunks}/${totalChunks}`,
        itemProgressPercent: progress,
        softCapPercent: 82,
        stageStartedAt: llmStageStartedAt,
        metrics: {
          candidateCount: mergedCandidates.length,
          judgedCount: Math.min(mergedCandidates.length, processedCandidates || totalCandidates || 0),
        },
        currentStep: {
          taskId: ACTIVE_SEARCH_TASK_ID || "",
          materialId: params.materialId,
          materialName: process.env.SEARCH_TASK_CURRENT_MATERIAL_NAME || "",
          stageOrder: params.stage.stageOrder,
          stageLabel: params.stage.label,
          stageKind: params.stage.kind,
          stepKey: "llm_match",
          stepLabel: "LLM 商品匹配候选筛选",
          startedAt: llmMatchStepStartedAt,
          endedAt: null,
          durationMs: null,
          elapsedMs: Math.max(0, Date.now() - Date.parse(llmMatchStepStartedAt)),
          status: "running",
        },
      });
    },
  });
  emitStepFinished({
    stage: params.stage,
    materialId: params.materialId,
    stepKey: "llm_match",
    stepLabel: "LLM 商品匹配候选筛选",
    startedAt: llmMatchStepStartedAt,
    itemProgressPercent: 82,
    softCapPercent: 85,
  });
  await assertActiveSearchTask(true);
  emitSearchProgress({
    message: "大模型商品匹配判断完成，正在整理候选商家...",
    itemProgressPercent: 82,
    softCapPercent: 85,
    metrics: {
      candidateCount: mergedCandidates.length,
      judgedCount: llmMatchedCandidates.filter((candidate) => candidate.judge_status === "judged").length,
      nameMatchedCount: llmMatchedCandidates.filter((candidate) => candidate.judge_status === "judged" && candidate.is_match === true).length,
    },
  });
  await writeFile(
    llmMatchResultsPath,
    JSON.stringify(llmMatchedCandidates.map((candidate) => ({
      input: candidate.input,
      "title-text": candidate["title-text"],
      url: candidate.url,
      base_score: candidate.base_score,
      score: candidate.score,
      confidence: candidate.confidence,
      is_match: candidate.is_match,
      match_bucket: candidate.match_bucket,
      reason: candidate.reason,
      judge_status: candidate.judge_status,
      llm_provider: candidate.llm_provider,
      llm_model: candidate.llm_model,
      llm_chunk_id: candidate.llm_chunk_id,
      llm_custom_id: candidate.llm_custom_id,
      search_round: candidate.search_round,
      round_label: candidate.round_label,
      source_product: candidate.source_product,
      merchant_name: candidate.source_product.merchant_name,
      merchant_url: candidate.source_product.merchant_url,
      image_url: candidate.source_product.image_url,
      wangwang_url: candidate.source_product.wangwang_url,
      contact_page_url: candidate.source_product.contact_page_url,
    })), null, 2),
    "utf8",
  );
  const hitRateStats = computeSemanticCandidateHitRate(llmMatchedCandidates);
  const hitRateReason = [
    `matched=${hitRateStats.matched_count}`,
    `judged=${hitRateStats.judged_count}`,
    `unjudged=${hitRateStats.unjudged_count}`,
    `errors=${hitRateStats.error_count}`,
  ].join(" ");

  const { matchedCandidates, possibleCandidates } = splitFixedRuntimeCandidatesByMatchBucket(llmMatchedCandidates);
  const judgedCandidateCount = llmMatchedCandidates.filter((candidate) => candidate.judge_status === "judged").length;
  const nameMatchedCount = matchedCandidates.length;
  const llmErrorCount = llmMatchedCandidates.filter((candidate) => candidate.judge_status === "llm_error").length;
  const llmOnlineFailed = mergedCandidates.length > 0 && judgedCandidateCount === 0 && llmErrorCount > 0;
  const screenshotDir = path.join(params.search.searchRunDir, "final-selected-offers");
  const contactProfileDir = resolve1688ProfileDir();
  await mkdir(screenshotDir, { recursive: true });
  const contactEnrichmentResult: ContactEnrichmentResult = matchedCandidates.length > 0
    ? await (async () => {
      const contactStepStartedAt = emitStepStarted({
        stage: params.stage,
        materialId: params.materialId,
        stepKey: "contact_enrichment",
        stepLabel: "联系方式补全/有效商家筛选",
        itemProgressPercent: 85,
        softCapPercent: 95,
      }).startedAt;
      emitSearchProgress({
        message: "正在按大模型匹配顺序补齐商家电话/手机...",
        itemProgressPercent: 85,
        softCapPercent: 95,
        metrics: {
          candidateCount: mergedCandidates.length,
          judgedCount: judgedCandidateCount,
          nameMatchedCount,
          attemptedContactCount: 0,
          qualifiedResultCount: 0,
          targetResultCount: targetQualifiedCount ?? null,
        },
      });
      const result = await enrichSelectedOfferCandidates({
        candidates: matchedCandidates,
        screenshotDir,
        headless: params.headless,
        profileDir: contactProfileDir,
        requireLoggedIn: true,
        targetQualifiedCount,
        auditPath: contactAuditPath,
        onCandidateAttempt: async ({ attemptedCount, qualifiedCount, targetQualifiedCount, status }) => {
          await assertActiveSearchTask();
          const targetText = targetQualifiedCount ?? matchedCandidates.length;
          const progress = Math.min(95, 85 + Math.floor((qualifiedCount / Math.max(1, targetText)) * 10));
          const statusText = status === "started"
            ? "正在抓取"
            : status === "qualified"
              ? "已获得有效商家"
              : status === "rejected_contact_error"
                ? "当前候选抓取失败，继续补位"
                : "当前候选无完整电话/手机，继续补位";
          emitSearchProgress({
            message: `${statusText}: 已尝试 ${attemptedCount} 个 LLM 匹配候选，已获得 ${qualifiedCount}/${targetText} 个有效商家`,
            itemProgressPercent: progress,
            softCapPercent: 95,
            metrics: {
              candidateCount: mergedCandidates.length,
              judgedCount: judgedCandidateCount,
              nameMatchedCount,
              attemptedContactCount: attemptedCount,
              qualifiedResultCount: qualifiedCount,
              targetResultCount: targetQualifiedCount ?? null,
            },
            currentStep: {
              taskId: ACTIVE_SEARCH_TASK_ID || "",
              materialId: params.materialId,
              materialName: process.env.SEARCH_TASK_CURRENT_MATERIAL_NAME || "",
              stageOrder: params.stage.stageOrder,
              stageLabel: params.stage.label,
              stageKind: params.stage.kind,
              stepKey: "contact_enrichment",
              stepLabel: "联系方式补全/有效商家筛选",
              startedAt: contactStepStartedAt,
              endedAt: null,
              durationMs: null,
              elapsedMs: Math.max(0, Date.now() - Date.parse(contactStepStartedAt)),
              status: "running",
            },
          });
        },
      });
      emitStepFinished({
        stage: params.stage,
        materialId: params.materialId,
        stepKey: "contact_enrichment",
        stepLabel: "联系方式补全/有效商家筛选",
        startedAt: contactStepStartedAt,
        itemProgressPercent: 95,
        softCapPercent: 96,
      });
      return result;
    })()
    : {
      enriched: [],
      attempts: [],
      attemptedCount: 0,
      attemptedCandidateCount: 0,
      qualifiedCount: 0,
      targetQualifiedCount: targetQualifiedCount ?? null,
      initialWindowSize: 0,
      duplicateMerchantCount: 0,
      shortageReason: targetQualifiedCount
        ? (mergedCandidates.length === 0
            ? "no_search_candidates"
            : llmOnlineFailed
              ? "llm_online_failed"
              : "matched_candidates_exhausted_or_valid_phone_insufficient")
        : "",
    };
  const remainingPossibleTargetQualifiedCount = targetQualifiedCount != null && contactEnrichmentResult.qualifiedCount >= targetQualifiedCount
    ? 0
    : possibleTargetQualifiedCount;
  if (possibleTargetQualifiedCount > 0 && remainingPossibleTargetQualifiedCount === 0 && possibleCandidates.length > 0) {
    syncLog(`可能商家联系方式补齐跳过：已获得目标有效商家 ${contactEnrichmentResult.qualifiedCount}/${targetQualifiedCount ?? contactEnrichmentResult.qualifiedCount}`);
  }
  const possibleContactEnrichmentResult = await enrichPossibleContactCandidates({
    candidates: possibleCandidates,
    screenshotDir,
    headless: params.headless,
    profileDir: contactProfileDir,
    targetQualifiedCount: remainingPossibleTargetQualifiedCount,
    auditPath: possibleContactAuditPath,
  });
  emitSearchProgress({
    message: matchedCandidates.length === 0
      ? "未获得名称匹配候选，正在整理搜索结果..."
      : "联系方式补齐完成，正在整理搜索结果...",
    itemProgressPercent: 95,
    softCapPercent: 96,
    metrics: {
      candidateCount: mergedCandidates.length,
      judgedCount: judgedCandidateCount,
      nameMatchedCount,
      attemptedContactCount: contactEnrichmentResult.attemptedCount,
      qualifiedResultCount: contactEnrichmentResult.qualifiedCount,
      targetResultCount: contactEnrichmentResult.targetQualifiedCount,
    },
  });
  if (matchedCandidates.length === 0) {
    await writeFile(contactAuditPath, JSON.stringify(contactEnrichmentResult, null, 2), "utf8");
  }
  const enrichedSelectedProducts: EnrichedSelectedOffer[] = contactEnrichmentResult.enriched;
  const possibleEnrichedSelectedProducts: EnrichedSelectedOffer[] = possibleContactEnrichmentResult.enriched;
  const shortageReason = contactEnrichmentResult.shortageReason
    || (targetQualifiedCount != null && enrichedSelectedProducts.length < targetQualifiedCount
      ? (mergedCandidates.length === 0
          ? "no_search_candidates"
          : llmOnlineFailed
            ? "llm_online_failed"
            : "matched_candidates_exhausted_or_valid_phone_insufficient")
      : "");
  if (targetQualifiedCount != null && enrichedSelectedProducts.length < targetQualifiedCount) {
    syncLog(
      `固定运行时电话补齐不足: 目标=${targetQualifiedCount} 合格=${enrichedSelectedProducts.length} LLM匹配候选=${matchedCandidates.length}，原因=${shortageReason || "unknown"}`,
    );
  }
  const diagnosticsStepStartedAt = emitStepStarted({
    stage: params.stage,
    materialId: params.materialId,
    stepKey: "stage_diagnostics_sync",
    stepLabel: "阶段诊断信息同步",
    itemProgressPercent: 95,
    softCapPercent: 97,
  }).startedAt;
  await trySync("fixed_runtime_stage_diagnostics", async () => {
    await updateSearchRoundStageDiagnostics({
      batchId: params.batchId,
      materialId: params.materialId,
      stageOrder: params.stage.stageOrder,
      taskId: ACTIVE_SEARCH_TASK_ID || null,
      sourceCandidateCount: mergedCandidates.length,
      keywordHitRate: hitRateStats.hit_rate,
      keywordHitRateSource: "llm_semantic_match",
      keywordHitRateReason: hitRateReason,
      keywordHitRateSampleCount: hitRateStats.judged_count,
      targetResultCount: targetQualifiedCount ?? null,
      shortageReason,
      contactEnrichmentAttempted: matchedCandidates.length > 0,
      contactEnrichmentSkipReason: matchedCandidates.length > 0 ? "" : (shortageReason || "llm_no_matches"),
    });
  });
  emitStepFinished({
    stage: params.stage,
    materialId: params.materialId,
    stepKey: "stage_diagnostics_sync",
    stepLabel: "阶段诊断信息同步",
    startedAt: diagnosticsStepStartedAt,
    itemProgressPercent: 96,
    softCapPercent: 98,
  });
  emitSearchProgress({
    message: "商家电话补齐完成，正在写入最终搜索结果...",
    itemProgressPercent: 96,
    softCapPercent: 98,
    metrics: {
      candidateCount: mergedCandidates.length,
      judgedCount: judgedCandidateCount,
      nameMatchedCount,
      attemptedContactCount: contactEnrichmentResult.attemptedCount,
      qualifiedResultCount: contactEnrichmentResult.qualifiedCount,
      targetResultCount: contactEnrichmentResult.targetQualifiedCount,
    },
  });
  const finalArtifactStepStartedAt = emitStepStarted({
    stage: params.stage,
    materialId: params.materialId,
    stepKey: "final_artifact_write",
    stepLabel: "最终结果文件写入",
    itemProgressPercent: 96,
    softCapPercent: 98,
  }).startedAt;

  const finalProducts = [
    ...enrichedSelectedProducts.map((product, index) => ({
    ...product,
    index: index + 1,
    source: "fixed_runtime_search",
    main_keyword: product.input,
    stage_order: params.stage.stageOrder,
    stage_label: params.stage.label,
    stage_kind: params.stage.kind,
    stage_main_keyword: product.input,
    stage_keyword_hit_rate: hitRateStats.hit_rate,
    stage_rank: index + 1,
    is_placeholder: false,
    placeholder_reason: "",
    match_bucket: "matched",
  })),
    ...possibleEnrichedSelectedProducts.map((product, index) => ({
      ...product,
      index: enrichedSelectedProducts.length + index + 1,
      source: "fixed_runtime_search",
      main_keyword: product.input,
      stage_order: params.stage.stageOrder,
      stage_label: params.stage.label,
      stage_kind: params.stage.kind,
      stage_main_keyword: product.input,
      stage_keyword_hit_rate: hitRateStats.hit_rate,
      stage_rank: enrichedSelectedProducts.length + index + 1,
      is_placeholder: false,
      placeholder_reason: "",
      is_match: false,
      match_bucket: "possible",
    })),
  ];
  await writeFile(finalSelectedProductsPath, JSON.stringify(finalProducts, null, 2), "utf8");
  const fixedRuntimeDiagnosticsDir = path.join(OUTPUT_ROOT, "fixed-runtime-diagnostics", ACTIVE_SEARCH_TASK_ID || "manual-run");
  await mkdir(fixedRuntimeDiagnosticsDir, { recursive: true });
  const diagnosticsSummaryPath = path.join(
    fixedRuntimeDiagnosticsDir,
    `${params.batchId}-${params.materialId}-fixed-runtime-diagnostics.json`,
  );
  await writeFile(
    diagnosticsSummaryPath,
    JSON.stringify(buildFixedRuntimeDiagnosticsSummary({
      materialId: params.materialId,
      batchId: params.batchId,
      shortageReason,
      targetQualifiedCount,
      llmMatchedCount: matchedCandidates.length,
      llmPossibleCount: possibleCandidates.length,
      mergedCandidateCount: mergedCandidates.length,
      qualifiedContactCount: enrichedSelectedProducts.length + possibleEnrichedSelectedProducts.length,
      attemptedContactCount: contactEnrichmentResult.attemptedCount + possibleContactEnrichmentResult.attemptedCount,
      contactEnrichmentResult,
      possibleContactEnrichmentResult,
      llmCandidates: llmMatchedCandidates as unknown as Array<Record<string, unknown>>,
      finalProducts,
      artifacts: {
        llmMatchResultsPath,
        contactEnrichmentResultsPath: contactAuditPath,
        possibleContactEnrichmentResultsPath: possibleContactAuditPath,
        finalSelectedProductsPath,
      },
    }), null, 2),
    "utf8",
  );
  emitStepFinished({
    stage: params.stage,
    materialId: params.materialId,
    stepKey: "final_artifact_write",
    stepLabel: "最终结果文件写入",
    startedAt: finalArtifactStepStartedAt,
    itemProgressPercent: 98,
    softCapPercent: 99,
  });

  return {
    source: "fixed_runtime_search",
    mainKeyword: buildFixedRuntimeMainKeyword(roundStatuses),
    candidateCount: mergedCandidates.length,
    duplicateCount: Math.max(0, roundCandidates.length - mergedCandidates.length) + contactEnrichmentResult.duplicateMerchantCount,
    hitRate: hitRateStats.hit_rate,
    llmMatchedCount: matchedCandidates.length,
    attemptedContactCount: contactEnrichmentResult.attemptedCount,
    qualifiedContactCount: enrichedSelectedProducts.length,
    targetResultCount: targetQualifiedCount ?? null,
    shortageReason,
    artifacts: {
      llmMatchResultsPath,
      contactEnrichmentResultsPath: contactAuditPath,
      finalSelectedProductsPath,
      diagnosticsSummaryPath,
    },
    finalProducts,
  };
}

async function resolveKeywordStageProducts(search: InitialSearchResult, limit: number): Promise<StageOutcome> {
  // 聚合所有轮次的结果（每轮都已持久化），按商家去重累计
  const allRoundsWithResults = search.initialRounds.filter(
    (round) => String(round.results_path ?? "").length > 0,
  );
  const preferredRound = pickPreferredStageRound(search);
  const mainKeyword = String(
    preferredRound?.executed_keyword
      ?? preferredRound?.primary_keyword
      ?? buildFallbackMainKeyword(search),
  );
  const stageKeywordHitRate = normalizeNullableNumber(preferredRound?.keyword_hit_rate);
  const stopPipeline = Boolean(preferredRound?.brand_zero_hit_stop);

  if (stopPipeline) {
    return {
      source: "keyword_search",
      mainKeyword,
      products: [],
      stageKeywordHitRate,
      stopPipeline: true,
      placeholderReason: "brand_zero_hit_stop",
    };
  }

  if (allRoundsWithResults.length > 0) {
    const seenKeys = new Set<string>();
    const merged: ReturnType<typeof normalizeProductFromRaw>[] = [];

    for (const round of allRoundsWithResults) {
      const roundProducts = await readResultsFromPath(String(round.results_path ?? ""), limit);
      for (const product of roundProducts) {
        const key = buildMerchantKey(product);
        if (key && seenKeys.has(key)) continue;
        if (key) seenKeys.add(key);
        merged.push(product);
      }
    }

    return {
      source: "keyword_search",
      mainKeyword,
      products: merged,
      stageKeywordHitRate,
      stopPipeline: false,
      placeholderReason: "",
    };
  }

  // 兜底：所有轮次 results_path 为空时，从 search_rounds 的 results JSON 字段提取产品
  const roundsWithInlineResults = search.initialRounds.filter((round) => {
    const results = round.results as unknown[];
    return Array.isArray(results) && results.length > 0;
  });
  if (roundsWithInlineResults.length > 0) {
    syncLog(`resolveKeywordStageProducts: 所有 results_path 为空，从 ${roundsWithInlineResults.length} 个轮次的内联 results 字段恢复`);
    const seenKeys = new Set<string>();
    const merged: ReturnType<typeof normalizeProductFromRaw>[] = [];
    for (const round of roundsWithInlineResults) {
      const rawProducts = applyMerchantLimit(round.results as Record<string, unknown>[], limit);
      for (const [index, item] of rawProducts.entries()) {
        const product = normalizeProductFromRaw(item, index + 1);
        const key = buildMerchantKey(product);
        if (key && seenKeys.has(key)) continue;
        if (key) seenKeys.add(key);
        merged.push(product);
      }
    }
    if (merged.length > 0) {
      return {
        source: "keyword_search_inline_fallback",
        mainKeyword,
        products: merged,
        stageKeywordHitRate,
        stopPipeline: false,
        placeholderReason: "",
      };
    }
  }

  const extendedBestMatchProducts = await readExtendedBestMatchProducts(search.searchRunDir, limit);
  if (extendedBestMatchProducts.length > 0) {
    return {
      source: "extended_best_match_fallback",
      mainKeyword,
      products: extendedBestMatchProducts,
      stageKeywordHitRate,
      stopPipeline: false,
      placeholderReason: "",
    };
  }

  const placeholderReason = resolveKeywordStagePlaceholderReason(search);
  if (placeholderReason) {
    return {
      source: "keyword_search",
      mainKeyword,
      products: [],
      stageKeywordHitRate,
      stopPipeline: false,
      placeholderReason,
    };
  }

  return {
    source: "extended_best_match_fallback",
    mainKeyword,
    products: [],
    stageKeywordHitRate,
    stopPipeline: false,
    placeholderReason: "",
  };
}

async function resolveImageStageProducts(
  search: Pick<InitialSearchResult, "searchRunDir" | "bestMatchImage" | "validRound" | "initialRounds">,
  img: ImageSearchResult,
  limit: number,
): Promise<StageOutcome> {
  const mainKeyword = buildFallbackMainKeyword(search);
  if (img.isValidImageSearch) {
    return {
      source: "image_search",
      mainKeyword,
      products: await readImageSearchProducts(img.imageSearchReportPath, limit),
      stageKeywordHitRate: null,
      stopPipeline: false,
      placeholderReason: "",
    };
  }

  return {
    source: "extended_best_match_fallback",
    mainKeyword,
    products: await readExtendedBestMatchProducts(search.searchRunDir, limit),
    stageKeywordHitRate: null,
    stopPipeline: false,
    placeholderReason: "",
  };
}

export async function resolveImageStageOutcome(params: {
  search: Pick<InitialSearchResult, "searchRunDir" | "bestMatchImage" | "validRound" | "initialRounds">;
  imageResult: ImageSearchResult | null;
  limit: number;
}): Promise<StageOutcome> {
  if (!params.imageResult) {
    return {
      source: "extended_best_match_fallback",
      mainKeyword: buildFallbackMainKeyword(params.search),
      products: await readExtendedBestMatchProducts(params.search.searchRunDir, params.limit),
      stageKeywordHitRate: null,
      stopPipeline: false,
      placeholderReason: "",
    };
  }

  return resolveImageStageProducts(params.search, params.imageResult, params.limit);
}

function buildAggregateFinalResultPayload(
  materialId: string,
  accumulatedProducts: StageResultProduct[],
  completedStages: CompletedStageSnapshot[],
): FinalResultPayload {
  return buildFinalResultPayload(
    materialId,
    completedStages.length > 1 ? "multi_stage_search" : (completedStages[0]?.source ?? "multi_stage_search"),
    completedStages.map((stage) => stage.mainKeyword).filter(Boolean).join(" | "),
    accumulatedProducts,
  );
}

function normalizeNullableNumber(value: unknown): number | null {
  if (value == null || value === "") {
    return null;
  }

  const numeric = Number(value);
  return Number.isFinite(numeric) ? numeric : null;
}

function pickPreferredStageRound(search: Pick<InitialSearchResult, "initialRounds" | "validRound">) {
  if (search.initialRounds.length === 0) {
    return null;
  }

  return search.initialRounds.find((round) => Boolean(round.brand_zero_hit_stop))
    ?? search.validRound
    ?? search.initialRounds.at(-1)
    ?? search.initialRounds[0]
    ?? null;
}

export function resolveKeywordStagePlaceholderReason(search: Pick<InitialSearchResult, "initialRounds" | "validRound">) {
  const preferredRound = pickPreferredStageRound(search);
  const roundFailureReason = normalizeSearchPlanRoundFailureReason(
    preferredRound?.round_failure_reason ?? preferredRound?.contact_enrichment_skip_reason,
  );

  if (roundFailureReason === "captcha_intercept") {
    return roundFailureReason;
  }

  if (roundFailureReason === "browser_target_crashed") {
    return roundFailureReason;
  }

  return "";
}

async function syncAccumulatedStageResult(params: {
  materialId: string;
  batchId: string;
  stage: SearchExecutionStage;
  source: string;
  mainKeyword: string;
  products: ReturnType<typeof normalizeProductFromRaw>[];
  stageKeywordHitRate: number | null;
  placeholderReason?: string;
  stageTiming?: SearchStageTiming | null;
  accumulatedProducts: StageResultProduct[];
  persistedStageProducts: StageResultProduct[];
  completedStages: CompletedStageSnapshot[];
}) {
  const qualifiedProducts = params.products.filter((product) => (
    product.has_phone_contact || hasDetectedPhoneContact(product.detected_contacts)
  ));
  const rejectedNoPhoneProducts = params.products.filter((product) => !(
    product.has_phone_contact || hasDetectedPhoneContact(product.detected_contacts)
  ));

  if (rejectedNoPhoneProducts.length > 0) {
    syncLog(`阶段 ${params.stage.stageOrder} ${params.stage.label}: ${rejectedNoPhoneProducts.length} 个商家无号码（仅审计保留）`);
  }

  const qualifiedStageProducts = annotateStageProducts(qualifiedProducts, {
    source: params.source,
    mainKeyword: params.mainKeyword,
    stageLabel: params.stage.label,
    stageOrder: params.stage.stageOrder,
    stageKind: params.stage.kind,
    stageKeywordHitRate: params.stageKeywordHitRate,
  });
  const rejectedStageProducts = annotateStageProducts(
    rejectedNoPhoneProducts.map((product) => ({
      ...product,
      has_phone_contact: false,
      qualification_status: String(product.qualification_status || "rejected_no_phone"),
    })),
    {
      source: params.source,
      mainKeyword: params.mainKeyword,
      stageLabel: params.stage.label,
      stageOrder: params.stage.stageOrder,
      stageKind: params.stage.kind,
      stageKeywordHitRate: params.stageKeywordHitRate,
    },
  );

  let nextAccumulatedProducts = params.accumulatedProducts;
  let nextPersistedStageProducts = params.persistedStageProducts;
  let insertedCount = 0;
  let duplicateCount = 0;

  if (params.placeholderReason) {
    nextPersistedStageProducts = [
      ...params.persistedStageProducts,
      buildPlaceholderStageProduct({
        source: params.source,
        mainKeyword: params.mainKeyword,
        stageLabel: params.stage.label,
        stageOrder: params.stage.stageOrder,
        stageKind: params.stage.kind,
        stageKeywordHitRate: params.stageKeywordHitRate,
        placeholderReason: params.placeholderReason,
      }),
    ];
  } else {
    const mergeResult = mergeStageProducts(params.accumulatedProducts, qualifiedStageProducts);
    nextAccumulatedProducts = mergeResult.merged;
    nextPersistedStageProducts = [
      ...params.persistedStageProducts,
      ...mergeResult.inserted,
      ...rejectedStageProducts,
    ];
    insertedCount = mergeResult.insertedCount;
    duplicateCount = mergeResult.duplicateCount;
  }

  const completedStage: CompletedStageSnapshot = {
    label: params.stage.label,
    source: params.source,
    mainKeyword: params.mainKeyword,
    candidateCount: params.products.length,
    insertedCount,
    duplicateCount,
    keywordHitRate: params.stageKeywordHitRate,
    stopReason: params.placeholderReason ?? "",
    timing: params.stageTiming ?? null,
  };
  const completedStages = [...params.completedStages, completedStage];
  await syncFinalResult(
    params.materialId,
    params.batchId,
    buildAggregateFinalResultPayload(params.materialId, nextAccumulatedProducts, completedStages),
    nextPersistedStageProducts,
  );
  syncLog(
    `阶段 ${params.stage.stageOrder} ${params.stage.label}: 候选=${params.products.length} 有效=${qualifiedStageProducts.length} 过滤=${rejectedStageProducts.length} 新增=${insertedCount} 重复=${duplicateCount} 命中率=${params.stageKeywordHitRate != null ? `${(params.stageKeywordHitRate * 100).toFixed(1)}%` : "-"}${params.placeholderReason ? ` | 停止=${params.placeholderReason}` : ""} 累计=${nextAccumulatedProducts.length}`,
  );

  return {
    accumulatedProducts: nextAccumulatedProducts,
    persistedStageProducts: nextPersistedStageProducts,
    completedStages,
  };
}

async function main() {
  const options = parseArgs(process.argv.slice(2));
  const pipelineConfig = parsePipelineConfig();
  const executionStages = buildSearchExecutionStages(pipelineConfig);
  const keywordArtifactConfig = buildKeywordArtifactPipelineConfig(pipelineConfig);
  syncLog(`开始同步模式 material_id=${options.materialId} batch_id=${options.batchId}`);
  syncLog(`管道配置: keywordSearch=${pipelineConfig.keywordSearchEnabled} imageSearch=${pipelineConfig.imageSearchEnabled} combinations=${pipelineConfig.keywordCombinations.length} aiKeyword=${pipelineConfig.includeAiKeyword}`);
  await cleanupTemporaryOutput();
  uploadedFileCache.clear();

  // ── 阶段 1：关键词生成
  const keywordResult = await stageKeywords(options.materialId, options.batchId || undefined, keywordArtifactConfig);
  await syncKeywordProcess(options.materialId, options.batchId, keywordResult);

  if (executionStages.length === 0) {
    await syncFinalResult(options.materialId, options.batchId, buildFinalResultPayload(options.materialId, "multi_stage_search", "", []));
    console.log("\n╔════════════════════ 正式联调结果（sync）════════════════════╗");
    console.log(`  物料ID:               ${options.materialId}`);
    console.log(`  流程状态:             未启用任何搜索阶段`);
    console.log(`  最终结果:             已同步 0 条商品`);
    console.log(`  Supabase:             已同步`);
    console.log("═══════════════════════════════════════════════════════════\n");
    return;
  }

  let searchRoundOffset = 0;
  let accumulatedProducts: StageResultProduct[] = [];
  let persistedStageProducts: StageResultProduct[] = [];
  let completedStages: CompletedStageSnapshot[] = [];
  let latestKeywordStage: LatestKeywordStageContext | null = null;

  for (const stage of executionStages) {
    await assertActiveSearchTask(true);
    syncLog(`开始阶段 ${stage.stageOrder}/${executionStages.length}: ${stage.label}`);
    const stageStartedAt = new Date().toISOString();
    emitStageStarted(stage, options.materialId, stageStartedAt);

    try {
      if (stage.kind === "keyword") {
        const keywordStepStartedAt = emitStepStarted({
          stage,
          materialId: options.materialId,
          stepKey: "keyword_generation",
          stepLabel: "关键词生成/读取",
          itemProgressPercent: 8,
          softCapPercent: 20,
        }).startedAt;
        const stageKeywordResult = await stageKeywords(options.materialId, options.batchId || undefined, stage.pipelineConfig);
        emitStepFinished({
          stage,
          materialId: options.materialId,
          stepKey: "keyword_generation",
          stepLabel: "关键词生成/读取",
          startedAt: keywordStepStartedAt,
          itemProgressPercent: 20,
          softCapPercent: 25,
        });
        const initialSearchStepStartedAt = emitStepStarted({
          stage,
          materialId: options.materialId,
          stepKey: "initial_search",
          stepLabel: "1688 初始搜索",
          itemProgressPercent: 22,
          softCapPercent: 35,
        }).startedAt;
        const search = await stageInitialSearch(options, stageKeywordResult, stage);
        emitStepFinished({
          stage,
          materialId: options.materialId,
          stepKey: "initial_search",
          stepLabel: "1688 初始搜索",
          startedAt: initialSearchStepStartedAt,
          itemProgressPercent: 34,
          softCapPercent: 38,
        });
        const searchSyncStepStartedAt = emitStepStarted({
          stage,
          materialId: options.materialId,
          stepKey: "search_result_sync",
          stepLabel: "搜索结果同步",
          itemProgressPercent: 35,
          softCapPercent: 42,
        }).startedAt;
        await syncInitialSearch(options.materialId, options.batchId, search, { searchRoundOffset, stage });
        emitStepFinished({
          stage,
          materialId: options.materialId,
          stepKey: "search_result_sync",
          stepLabel: "搜索结果同步",
          startedAt: searchSyncStepStartedAt,
          itemProgressPercent: 42,
          softCapPercent: 45,
        });
        searchRoundOffset += getKeywordStageSearchRoundIncrement({
          keywordResult: stageKeywordResult,
          initialRounds: search.initialRounds,
        });

        if (stage.mode === "fixed_runtime") {
          const fixedRuntimeResult = await resolveFixedRuntimeSelection({
            materialId: options.materialId,
            batchId: options.batchId,
            search,
            keywordResult: stageKeywordResult,
            stage,
            contactLimit: options.contactLimit,
            headless: options.headless,
          });
          const stageTiming = createStageTiming({ stage, materialId: options.materialId, startedAt: stageStartedAt });
          accumulatedProducts = fixedRuntimeResult.finalProducts as unknown as StageResultProduct[];
          persistedStageProducts = [...accumulatedProducts];
          completedStages = [
            ...completedStages,
            {
              label: stage.label,
              source: fixedRuntimeResult.source,
              mainKeyword: fixedRuntimeResult.mainKeyword,
              candidateCount: fixedRuntimeResult.candidateCount,
              insertedCount: fixedRuntimeResult.finalProducts.length,
              duplicateCount: fixedRuntimeResult.duplicateCount,
              keywordHitRate: fixedRuntimeResult.hitRate,
              stopReason: "",
              timing: stageTiming,
            },
          ];
          await updateSearchRoundStageTiming({
            batchId: options.batchId,
            materialId: options.materialId,
            stageOrder: stage.stageOrder,
            taskId: ACTIVE_SEARCH_TASK_ID || null,
            stageStartedAt: stageTiming.startedAt,
            stageEndedAt: stageTiming.endedAt,
            stageDurationMs: stageTiming.durationMs,
            stageStatus: stageTiming.status,
          });
          emitStageFinished(stage, stageTiming);
          await assertActiveSearchTask(true);
          const finalSyncStepStartedAt = emitStepStarted({
            stage,
            materialId: options.materialId,
            stepKey: "final_result_sync",
            stepLabel: "最终结果同步",
            itemProgressPercent: 98,
            softCapPercent: 99,
          }).startedAt;
          await syncFinalResult(
            options.materialId,
            options.batchId,
            buildFinalResultPayload(
              options.materialId,
              fixedRuntimeResult.source,
              fixedRuntimeResult.mainKeyword,
              fixedRuntimeResult.finalProducts,
            ),
            fixedRuntimeResult.finalProducts,
            await readSearchRoundTitleFallbackProducts(search.initialRounds as Record<string, unknown>[]),
          );
          emitStepFinished({
            stage,
            materialId: options.materialId,
            stepKey: "final_result_sync",
            stepLabel: "最终结果同步",
            startedAt: finalSyncStepStartedAt,
            itemProgressPercent: 99,
            softCapPercent: 100,
          });
          syncLog(
            `固定运行时阶段完成: 候选=${fixedRuntimeResult.candidateCount} 重复=${fixedRuntimeResult.duplicateCount} 命中率=${formatNullableHitRate(fixedRuntimeResult.hitRate)} 最终入围=${fixedRuntimeResult.finalProducts.length}`,
          );
          break;
        }

        if (search.bestMatchImage?.image_path) {
          latestKeywordStage = { search, keywordResult: stageKeywordResult, stage };
        }

        const stageOutcome = await resolveKeywordStageProducts(search, options.contactLimit);
        const stageTiming = createStageTiming({ stage, materialId: options.materialId, startedAt: stageStartedAt });
        await updateSearchRoundStageTiming({
          batchId: options.batchId,
          materialId: options.materialId,
          stageOrder: stage.stageOrder,
          taskId: ACTIVE_SEARCH_TASK_ID || null,
          stageStartedAt: stageTiming.startedAt,
          stageEndedAt: stageTiming.endedAt,
          stageDurationMs: stageTiming.durationMs,
          stageStatus: stageTiming.status,
        });
        emitStageFinished(stage, stageTiming);
        await assertActiveSearchTask(true);
        const stageResultSyncStepStartedAt = emitStepStarted({
          stage,
          materialId: options.materialId,
          stepKey: "stage_result_sync",
          stepLabel: "阶段结果合并与落库",
          itemProgressPercent: 90,
          softCapPercent: 98,
        }).startedAt;
        ({ accumulatedProducts, persistedStageProducts, completedStages } = await syncAccumulatedStageResult({
          materialId: options.materialId,
          batchId: options.batchId,
          stage,
          source: stageOutcome.source,
          mainKeyword: stageOutcome.mainKeyword,
          products: stageOutcome.products,
          stageKeywordHitRate: stageOutcome.stageKeywordHitRate,
          placeholderReason: stageOutcome.placeholderReason,
          stageTiming,
          accumulatedProducts,
          persistedStageProducts,
          completedStages,
        }));
        emitStepFinished({
          stage,
          materialId: options.materialId,
          stepKey: "stage_result_sync",
          stepLabel: "阶段结果合并与落库",
          startedAt: stageResultSyncStepStartedAt,
          itemProgressPercent: 98,
          softCapPercent: 99,
        });

        if (stageOutcome.stopPipeline) {
          syncLog(`阶段 ${stage.stageOrder} ${stage.label} 触发停止条件，终止后续关键词阶段与图搜阶段`);
          break;
        }
        continue;
      }

      if (!latestKeywordStage) {
        syncLog(`阶段 ${stage.stageOrder} ${stage.label} 跳过：缺少可用的 best match 图片`);
        continue;
      }

      let img: ImageSearchResult | null = null;
      try {
        const imageSearchStepStartedAt = emitStepStarted({
          stage,
          materialId: options.materialId,
          stepKey: "image_search",
          stepLabel: "1688 图片搜索",
          itemProgressPercent: 45,
          softCapPercent: 70,
        }).startedAt;
        img = await stageImageSearch(
          options,
          latestKeywordStage.search.searchRunSummaryPath,
          latestKeywordStage.keywordResult,
        );
        emitStepFinished({
          stage,
          materialId: options.materialId,
          stepKey: "image_search",
          stepLabel: "1688 图片搜索",
          startedAt: imageSearchStepStartedAt,
          itemProgressPercent: 72,
          softCapPercent: 78,
        });
        const imageSyncStepStartedAt = emitStepStarted({
          stage,
          materialId: options.materialId,
          stepKey: "image_result_sync",
          stepLabel: "图搜结果同步",
          itemProgressPercent: 78,
          softCapPercent: 84,
        }).startedAt;
        await syncImageSearch(options.materialId, options.batchId, img);
        emitStepFinished({
          stage,
          materialId: options.materialId,
          stepKey: "image_result_sync",
          stepLabel: "图搜结果同步",
          startedAt: imageSyncStepStartedAt,
          itemProgressPercent: 84,
          softCapPercent: 88,
        });
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        syncLog(`阶段 ${stage.stageOrder} ${stage.label} 执行失败，回退到关键词阶段结果: ${message}`);
      }

      const stageOutcome = await resolveImageStageOutcome({
        search: latestKeywordStage.search,
        imageResult: img,
        limit: options.contactLimit,
      });
      const stageTiming = createStageTiming({ stage, materialId: options.materialId, startedAt: stageStartedAt });
      emitStageFinished(stage, stageTiming);
      await assertActiveSearchTask(true);
      const stageResultSyncStepStartedAt = emitStepStarted({
        stage,
        materialId: options.materialId,
        stepKey: "stage_result_sync",
        stepLabel: "阶段结果合并与落库",
        itemProgressPercent: 88,
        softCapPercent: 98,
      }).startedAt;
      ({ accumulatedProducts, persistedStageProducts, completedStages } = await syncAccumulatedStageResult({
        materialId: options.materialId,
        batchId: options.batchId,
        stage,
        source: stageOutcome.source,
        mainKeyword: stageOutcome.mainKeyword,
        products: stageOutcome.products,
        stageKeywordHitRate: stageOutcome.stageKeywordHitRate,
        stageTiming,
        accumulatedProducts,
        persistedStageProducts,
        completedStages,
      }));
      emitStepFinished({
        stage,
        materialId: options.materialId,
        stepKey: "stage_result_sync",
        stepLabel: "阶段结果合并与落库",
        startedAt: stageResultSyncStepStartedAt,
        itemProgressPercent: 98,
        softCapPercent: 99,
      });
    } catch (error) {
      emitStageFinished(stage, createStageTiming({
        stage,
        materialId: options.materialId,
        startedAt: stageStartedAt,
        status: "failed",
      }));
      throw error;
    }
  }

  if (completedStages.length === 0) {
    await syncFinalResult(options.materialId, options.batchId, buildFinalResultPayload(options.materialId, "multi_stage_search", "", []));
    console.log("\n╔════════════════════ 正式联调结果（sync）════════════════════╗");
    console.log(`  物料ID:               ${options.materialId}`);
    console.log(`  流程状态:             所有搜索阶段均被跳过`);
    console.log(`  最终结果:             已同步 0 条商品`);
    console.log(`  Supabase:             已同步`);
    console.log("═══════════════════════════════════════════════════════════\n");
    return;
  }

  console.log("\n╔════════════════════ 正式联调结果（sync）════════════════════╗");
  console.log(`  物料ID:               ${options.materialId}`);
  console.log(`  已执行阶段:           ${completedStages.map((stage) => stage.label).join(" → ")}`);
  console.log(`  阶段候选总数:         ${completedStages.reduce((sum, stage) => sum + stage.candidateCount, 0)}`);
  console.log(`  去重过滤数量:         ${completedStages.reduce((sum, stage) => sum + stage.duplicateCount, 0)}`);
  console.log(`  最终结果:             已同步 ${accumulatedProducts.length} 条商品`);
  console.log(`  Supabase:             已同步`);
  console.log("═══════════════════════════════════════════════════════════\n");
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
  main()
    .catch((error) => {
      console.error("\n❌ 同步模式执行失败:", error);
      process.exitCode = 1;
    })
    .finally(async () => {
      try {
        await cleanupTemporaryOutput();
        syncLog("本地临时 output 已保留");
      } catch (error) {
        const msg = error instanceof Error ? error.message : String(error);
        syncLog(`清理本地临时 output 失败: ${msg}`);
      }
    });
}
