/**
 * 批量准确度测试：10 组远距离美国地址 → MotherShip 联想 → RPA 报价 → docs/测试数据.md
 * 路径与 EmbeddedQuoteWidget 一致：
 *   POST /api/addresses/mothership-candidates → 确认候选 → POST /api/quotes → GET 轮询
 * 前置：npm run dev:start（Next + worker:rpa + worker-scheduler + Redis/MySQL）
 */
import { randomUUID } from "node:crypto";
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { loadDevEnv } from "@/lib/dev-env";
import { ensureNativeInfraIfNeeded } from "@/lib/infra/ensure-native-infra";
import {
  hostCreateQuote,
  hostFetchMothershipCandidates,
  hostGetQuote,
  hostPreheatQuoteSession,
} from "@/lib/frontend/api-client";
import { applyMothershipCandidate } from "@/lib/frontend/mothership-address";
import type {
  AddressInput,
  MothershipAddressCandidate,
  QuoteRequestBody,
} from "@/lib/frontend/types";
import { pollQuoteUntilDone } from "@/hooks/use-quote-polling";
import { isAnyRpaWorkerHealthy } from "@/lib/rpa/worker-health";
import { ensureRedisReady, resetRedisClient } from "@/lib/redis";
import { QUOTE_TIMEOUT_MS } from "@/lib/constants/quote";
import { recoverOrphanActiveJobs } from "@/workers/rpa/queue-recovery";

loadDevEnv();

const BASE = process.env.ACCURACY_TEST_BASE_URL ?? "http://localhost:3000";
const TOKEN = "demo-host-token";
const CUSTOMER_ID = "CUST_001";
const APP_READY_TIMEOUT_MS = 120_000;
const STACK_POLL_MS = 3_000;
const ACCURACY_TEST_QUOTE_TIMEOUT_MS = Number(
  process.env.ACCURACY_TEST_QUOTE_TIMEOUT_MS ?? QUOTE_TIMEOUT_MS,
);

type AddressDraft = {
  street: string;
  city: string;
  state: string;
};

type TestPair = {
  id: string;
  label: string;
  pickup: AddressDraft;
  delivery: AddressDraft;
  cargo: {
    pallet_count: number;
    weight_lb: number;
    length_in: number;
    width_in: number;
    height_in: number;
    cargo_type: string;
  };
};

const TEST_PAIRS: TestPair[] = [
  {
    id: "01",
    label: "洛杉矶 CA → Farmers Branch TX（基线）",
    pickup: { street: "1234 Warehouse Street", city: "Los Angeles", state: "CA" },
    delivery: { street: "5678 Distribution Way", city: "Farmers Branch", state: "TX" },
    cargo: { pallet_count: 2, weight_lb: 500, length_in: 48, width_in: 40, height_in: 48, cargo_type: "general_freight" },
  },
  {
    id: "02",
    label: "西雅图 WA → 迈阿密 FL",
    pickup: { street: "3131 Western Ave", city: "Seattle", state: "WA" },
    delivery: { street: "7000 NW 52nd St", city: "Miami", state: "FL" },
    cargo: { pallet_count: 3, weight_lb: 550, length_in: 48, width_in: 40, height_in: 45, cargo_type: "general_freight" },
  },
  {
    id: "03",
    label: "芝加哥 IL → 凤凰城 AZ",
    pickup: { street: "1500 W Fulton St", city: "Chicago", state: "IL" },
    delivery: { street: "4302 E Broadway Rd", city: "Phoenix", state: "AZ" },
    cargo: { pallet_count: 2, weight_lb: 600, length_in: 47, width_in: 40, height_in: 48, cargo_type: "machinery" },
  },
  {
    id: "04",
    label: "波士顿 MA → 休斯顿 TX",
    pickup: { street: "10 World Trade Center Ave", city: "Boston", state: "MA" },
    delivery: { street: "7701 Wallisville Rd", city: "Houston", state: "TX" },
    cargo: { pallet_count: 4, weight_lb: 650, length_in: 48, width_in: 42, height_in: 46, cargo_type: "general_freight" },
  },
  {
    id: "05",
    label: "波特兰 OR → 亚特兰大 GA",
    pickup: { street: "2735 NW 29th Ave", city: "Portland", state: "OR" },
    delivery: { street: "4000 Cumberland Blvd SE", city: "Atlanta", state: "GA" },
    cargo: { pallet_count: 2, weight_lb: 700, length_in: 46, width_in: 40, height_in: 48, cargo_type: "furniture" },
  },
  {
    id: "06",
    label: "丹佛 CO → 夏洛特 NC",
    pickup: { street: "4800 Race St", city: "Denver", state: "CO" },
    delivery: { street: "9300 South Blvd", city: "Charlotte", state: "NC" },
    cargo: { pallet_count: 3, weight_lb: 450, length_in: 48, width_in: 40, height_in: 44, cargo_type: "general_freight" },
  },
  {
    id: "07",
    label: "明尼阿波利斯 MN → 圣安东尼奥 TX",
    pickup: { street: "900 Olson Memorial Hwy", city: "Minneapolis", state: "MN" },
    delivery: { street: "6802 Bandera Rd", city: "San Antonio", state: "TX" },
    cargo: { pallet_count: 2, weight_lb: 520, length_in: 48, width_in: 40, height_in: 42, cargo_type: "auto_parts" },
  },
  {
    id: "08",
    label: "底特律 MI → 杰克逊维尔 FL",
    pickup: { street: "1900 Fort St", city: "Detroit", state: "MI" },
    delivery: { street: "6817 Southpoint Pkwy", city: "Jacksonville", state: "FL" },
    cargo: { pallet_count: 3, weight_lb: 580, length_in: 45, width_in: 40, height_in: 48, cargo_type: "general_freight" },
  },
  {
    id: "09",
    label: "盐湖城 UT → 费城 PA",
    pickup: { street: "2350 S 900 W", city: "Salt Lake City", state: "UT" },
    delivery: { street: "850 N 12th St", city: "Philadelphia", state: "PA" },
    cargo: { pallet_count: 2, weight_lb: 480, length_in: 48, width_in: 38, height_in: 48, cargo_type: "electronics" },
  },
  {
    id: "10",
    label: "堪萨斯城 MO → 圣迭戈 CA",
    pickup: { street: "6800 Kansas Ave", city: "Kansas City", state: "MO" },
    delivery: { street: "8585 Production Ave", city: "San Diego", state: "CA" },
    cargo: { pallet_count: 4, weight_lb: 620, length_in: 48, width_in: 40, height_in: 47, cargo_type: "building_materials" },
  },
];

type QuoteTier = {
  service_level: string;
  rate_option: string;
  carrier: string;
  transit_days: string;
  transit_description: string;
  raw_freight: number;
  raw_total: number;
  final_total: number;
};

type CaseResult = {
  pair: TestPair;
  startedAt: string;
  finishedAt: string;
  elapsedMs: number;
  pickupCandidates: MothershipAddressCandidate[];
  deliveryCandidates: MothershipAddressCandidate[];
  selectedPickup: MothershipAddressCandidate | null;
  selectedDelivery: MothershipAddressCandidate | null;
  quoteSessionId?: string;
  quoteId?: string;
  status: "done" | "failed" | "timeout" | "candidates_failed";
  sourceType?: string;
  isRealtime?: boolean;
  errorCode?: string;
  errorMessage?: string;
  quotes: QuoteTier[];
};


function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

/** 与 QuoteForm 提交前地址结构一致（街道/城市/州，无邮编） */
function buildDraftAddress(draft: AddressDraft): AddressInput {
  const street = draft.street.trim();
  const city = draft.city.trim();
  const state = draft.state.trim();
  return {
    street,
    city,
    state,
    zip: "",
    place_id: `draft_${street}_${city}_${state}`.replace(/\s+/g, "_").slice(0, 64),
    formatted_address: `${street}, ${city}, ${state}`,
    selected_from_suggestions: true,
  };
}

function buildBaseQuoteBody(pair: TestPair): QuoteRequestBody {
  return {
    request_id: randomUUID(),
    customer_id: CUSTOMER_ID,
    pickup_address: buildDraftAddress(pair.pickup),
    delivery_address: buildDraftAddress(pair.delivery),
    weight: { value: pair.cargo.weight_lb, unit: "lb" },
    dimensions: {
      length: pair.cargo.length_in,
      width: pair.cargo.width_in,
      height: pair.cargo.height_in,
      unit: "in",
    },
    pallet_count: pair.cargo.pallet_count,
    cargo_type: pair.cargo.cargo_type as QuoteRequestBody["cargo_type"],
  };
}

async function ensureInfraStableForCase(): Promise<void> {
  ensureNativeInfraIfNeeded();
  resetRedisClient();
  await ensureRedisReady(12, 1_000);
  const recovered = await recoverOrphanActiveJobs();
  if (recovered > 0) {
    console.warn(`[accuracy-test] 已回收 ${recovered} 条 orphan 队列 job`);
  }
}

async function ensureDevStackReady(): Promise<void> {
  ensureNativeInfraIfNeeded();
  try {
    await ensureInfraStableForCase();
  } catch {
    /* 栈未就绪时下面轮询会重试 */
  }
  const deadline = Date.now() + APP_READY_TIMEOUT_MS;
  let lastError = "未知";
  let stableHits = 0;
  while (Date.now() < deadline) {
    try {
      ensureNativeInfraIfNeeded();
      await ensureRedisReady(5, 500);
      const res = await fetch(BASE, { method: "GET", redirect: "manual" });
      if (res.status <= 0) {
        lastError = "Next 未响应";
        stableHits = 0;
      } else if (!(await isAnyRpaWorkerHealthy())) {
        lastError = "RPA Worker 无心跳（请 npm run dev:start）";
        stableHits = 0;
      } else {
        stableHits += 1;
        if (stableHits >= 2) {
          console.log("[accuracy-test] 全栈就绪：Next + Redis + RPA Worker");
          return;
        }
      }
    } catch (error) {
      lastError = error instanceof Error ? error.message : String(error);
      stableHits = 0;
      ensureNativeInfraIfNeeded();
      resetRedisClient();
    }
    await sleep(STACK_POLL_MS);
  }
  throw new Error(
    `开发栈未就绪（${lastError}）。请执行：npm run dev:infra:start && npm run dev:start`,
  );
}

function formatDraft(addr: AddressDraft): string {
  return `${addr.street}, ${addr.city}, ${addr.state}`;
}

function tierLabel(level: string, option: string): string {
  const levelMap: Record<string, string> = {
    standard: "标准",
    guaranteed: "保证送达",
  };
  const optionMap: Record<string, string> = {
    lowest: "最低价",
    fastest: "最快",
    bestValue: "最优性价比",
  };
  return `${levelMap[level] ?? level} / ${optionMap[option] ?? option}`;
}

async function fetchCandidatesViaHttp(
  baseBody: QuoteRequestBody,
): Promise<
  | { ok: true; data: MothershipCandidatesResult }
  | { ok: false; message: string }
> {
  const res = await hostFetchMothershipCandidates(
    BASE,
    TOKEN,
    CUSTOMER_ID,
    baseBody.pickup_address,
    baseBody.delivery_address,
  );
  if (res.code !== 0 || !res.data) {
    return { ok: false, message: res.message ?? "地址联想失败" };
  }
  return { ok: true, data: res.data };
}

async function submitAndPollQuote(
  body: QuoteRequestBody,
): Promise<{
  quoteId: string;
  status: string;
  sourceType?: string;
  isRealtime?: boolean;
  errorCode?: string;
  errorMessage?: string;
  quotes: QuoteTier[];
}> {
  const created = await hostCreateQuote(BASE, TOKEN, body);
  if (created.code !== 0 || !created.data?.quote_id) {
    throw new Error(created.message ?? "创建询价失败");
  }

  const quoteId = created.data.quote_id;

  if (created.data.status === "done") {
    const detail = await hostGetQuote(BASE, TOKEN, CUSTOMER_ID, quoteId);
    if (detail.code !== 0 || !detail.data) {
      throw new Error(detail.message ?? "查询报价失败");
    }
    return {
      quoteId,
      status: detail.data.status,
      sourceType: detail.data.source_type ?? undefined,
      isRealtime: detail.data.is_realtime,
      errorCode: detail.data.error_code ?? undefined,
      errorMessage: detail.data.error_message ?? undefined,
      quotes: detail.data.quotes ?? [],
    };
  }

  const pollResult = await pollQuoteUntilDone(async () => {
    const detail = await hostGetQuote(BASE, TOKEN, CUSTOMER_ID, quoteId);
    if (detail.code !== 0) {
      return { ok: false, errorMessage: detail.message };
    }
    return { ok: true, data: detail.data };
  }, { timeoutMs: ACCURACY_TEST_QUOTE_TIMEOUT_MS });

  if (pollResult.type === "done") {
    const q = pollResult.quote;
    return {
      quoteId,
      status: q.status,
      sourceType: q.source_type ?? undefined,
      isRealtime: q.is_realtime,
      errorCode: q.error_code ?? undefined,
      errorMessage: q.error_message ?? undefined,
      quotes: q.quotes ?? [],
    };
  }

  if (pollResult.type === "error") {
    return {
      quoteId,
      status: "failed",
      errorMessage: pollResult.message,
      quotes: [],
    };
  }

  return {
    quoteId,
    status: "timeout",
    quotes: [],
  };
}

async function runCase(pair: TestPair): Promise<CaseResult> {
  const startedAt = new Date().toISOString();
  const t0 = Date.now();
  console.log(`\n[${pair.id}] ${pair.label} — 地址联想（HTTP API）…`);

  await ensureInfraStableForCase();

  const baseBody = buildBaseQuoteBody(pair);
  let candidates;
  try {
    candidates = await fetchCandidatesViaHttp(baseBody);
  } catch (error) {
    return {
      pair,
      startedAt,
      finishedAt: new Date().toISOString(),
      elapsedMs: Date.now() - t0,
      pickupCandidates: [],
      deliveryCandidates: [],
      selectedPickup: null,
      selectedDelivery: null,
      status: "candidates_failed",
      errorMessage:
        error instanceof Error ? error.message : "地址联想请求失败",
      quotes: [],
    };
  }
  if (!candidates.ok) {
    return {
      pair,
      startedAt,
      finishedAt: new Date().toISOString(),
      elapsedMs: Date.now() - t0,
      pickupCandidates: [],
      deliveryCandidates: [],
      selectedPickup: null,
      selectedDelivery: null,
      status: "candidates_failed",
      errorMessage: candidates.message,
      quotes: [],
    };
  }

  const { pickup_candidates, delivery_candidates, quote_session_id } =
    candidates.data;
  if (quote_session_id) {
    await hostPreheatQuoteSession(BASE, TOKEN, CUSTOMER_ID, quote_session_id);
  }
  const selectedPickup = pickup_candidates[0] ?? null;
  const selectedDelivery = delivery_candidates[0] ?? null;

  if (!selectedPickup || !selectedDelivery) {
    return {
      pair,
      startedAt,
      finishedAt: new Date().toISOString(),
      elapsedMs: Date.now() - t0,
      pickupCandidates: pickup_candidates,
      deliveryCandidates: delivery_candidates,
      selectedPickup,
      selectedDelivery,
      quoteSessionId: quote_session_id,
      status: "candidates_failed",
      errorMessage: "联想结果为空，无法确认地址",
      quotes: [],
    };
  }

  console.log(
    `[${pair.id}] 确认联想：${selectedPickup.display_label} → ${selectedDelivery.display_label}`,
  );
  console.log(`[${pair.id}] 询价中…`);

  const confirmedBody: QuoteRequestBody = {
    ...baseBody,
    request_id: randomUUID(),
    quote_session_id,
    pickup_address: applyMothershipCandidate(
      baseBody.pickup_address,
      selectedPickup,
    ),
    delivery_address: applyMothershipCandidate(
      baseBody.delivery_address,
      selectedDelivery,
    ),
  };

  let quoteResult;
  try {
    quoteResult = await submitAndPollQuote(confirmedBody);
  } catch (error) {
    return {
      pair,
      startedAt,
      finishedAt: new Date().toISOString(),
      elapsedMs: Date.now() - t0,
      pickupCandidates: pickup_candidates,
      deliveryCandidates: delivery_candidates,
      selectedPickup,
      selectedDelivery,
      quoteSessionId: quote_session_id,
      status: "failed",
      errorMessage: error instanceof Error ? error.message : "询价异常",
      quotes: [],
    };
  }

  const status =
    quoteResult.status === "done"
      ? "done"
      : quoteResult.status === "timeout"
        ? "timeout"
        : "failed";

  console.log(
    `[${pair.id}] 完成 status=${status} tiers=${quoteResult.quotes.length} quote_id=${quoteResult.quoteId}`,
  );

  return {
    pair,
    startedAt,
    finishedAt: new Date().toISOString(),
    elapsedMs: Date.now() - t0,
    pickupCandidates: pickup_candidates,
    deliveryCandidates: delivery_candidates,
    selectedPickup,
    selectedDelivery,
    quoteSessionId: quote_session_id,
    quoteId: quoteResult.quoteId,
    status,
    sourceType: quoteResult.sourceType,
    isRealtime: quoteResult.isRealtime,
    errorCode: quoteResult.errorCode,
    errorMessage: quoteResult.errorMessage,
    quotes: quoteResult.quotes,
  };
}

function renderCandidateList(list: MothershipAddressCandidate[]): string {
  if (list.length === 0) {
    return "- （无候选）\n";
  }
  return list
    .map(
      (c, i) =>
        `${i + 1}. \`${c.display_label}\`（option_id: \`${c.option_id}\`，zip: ${c.zip || "—"}）`,
    )
    .join("\n");
}

function renderMarkdown(results: CaseResult[], generatedAt: string): string {
  const doneCount = results.filter((r) => r.status === "done").length;
  const lines: string[] = [
    "# 查价准确度测试数据",
    "",
    `> 生成时间：${generatedAt}`,
    `> 测试组数：10 | 成功报价：${doneCount} | 数据源：MotherShip RPA（RPA_MOCK_MODE=false）`,
    `> 查询路径：与内嵌组件相同 — POST /api/addresses/mothership-candidates → POST /api/quotes → GET 轮询`,
    `> 说明：每组为远距离美国工业/物流类街道地址；联想候选取 **第 1 项** 作为确认地址（多候选时请人工核对列表）。`,
    "",
    "## 汇总",
    "",
    "| 组号 | 路线 | 状态 | 报价档位数 | 单号 | 耗时 |",
    "|------|------|------|-----------|------|------|",
  ];

  for (const r of results) {
    lines.push(
      `| ${r.pair.id} | ${r.pair.label} | ${r.status} | ${r.quotes.length} | ${r.quoteId ?? "—"} | ${Math.round(r.elapsedMs / 1000)}s |`,
    );
  }

  lines.push("", "---", "");

  for (const r of results) {
    lines.push(`## 测试组 ${r.pair.id}：${r.pair.label}`, "");
    lines.push("### 用户填写地址", "");
    lines.push("| 项目 | 内容 |");
    lines.push("|------|------|");
    lines.push(`| 提货街道 | ${r.pair.pickup.street} |`);
    lines.push(`| 提货城市/州 | ${r.pair.pickup.city}, ${r.pair.pickup.state} |`);
    lines.push(`| 派送街道 | ${r.pair.delivery.street} |`);
    lines.push(`| 派送城市/州 | ${r.pair.delivery.city}, ${r.pair.delivery.state} |`);
    lines.push("");

    lines.push("### MotherShip 联想候选", "");
    lines.push("#### 提货候选", "");
    lines.push(renderCandidateList(r.pickupCandidates), "");
    lines.push("#### 派送候选", "");
    lines.push(renderCandidateList(r.deliveryCandidates), "");

    lines.push("### 本次确认使用的联想地址", "");
    if (r.selectedPickup && r.selectedDelivery) {
      lines.push(`- **提货**：\`${r.selectedPickup.display_label}\``);
      lines.push(`- **派送**：\`${r.selectedDelivery.display_label}\``);
      if (r.quoteSessionId) {
        lines.push(`- quote_session_id：\`${r.quoteSessionId}\``);
      }
    } else {
      lines.push("- （未确认）");
    }
    lines.push("");

    lines.push("### 货物信息", "");
    lines.push("| 项目 | 值 |");
    lines.push("|------|-----|");
    lines.push(`| 托盘数 | ${r.pair.cargo.pallet_count} |`);
    lines.push(`| 单托重量 | ${r.pair.cargo.weight_lb} lb |`);
    lines.push(
      `| 尺寸 (in) | ${r.pair.cargo.length_in} × ${r.pair.cargo.width_in} × ${r.pair.cargo.height_in} |`,
    );
    lines.push(`| 货物类型 | ${r.pair.cargo.cargo_type} |`);
    lines.push("");

    lines.push("### 报价结果", "");
    lines.push(`- **状态**：${r.status}`);
    if (r.quoteId) lines.push(`- **报价单号**：\`${r.quoteId}\``);
    if (r.sourceType) lines.push(`- **来源**：${r.sourceType}${r.isRealtime === false ? "（非实时）" : ""}`);
    if (r.errorCode) lines.push(`- **错误码**：${r.errorCode}`);
    if (r.errorMessage) lines.push(`- **错误信息**：${r.errorMessage}`);
    lines.push("");

    if (r.quotes.length > 0) {
      lines.push("| 档位 | 承运商 | 时效 | 原始总价 (USD) | 客户最终价 (USD) |");
      lines.push("|------|--------|------|----------------|------------------|");
      const sorted = [...r.quotes].sort((a, b) => {
        const ka = `${a.service_level}/${a.rate_option}`;
        const kb = `${b.service_level}/${b.rate_option}`;
        return ka.localeCompare(kb);
      });
      for (const q of sorted) {
        lines.push(
          `| ${tierLabel(q.service_level, q.rate_option)} | ${q.carrier} | ${q.transit_description || q.transit_days} | ${q.raw_total.toFixed(2)} | ${q.final_total.toFixed(2)} |`,
        );
      }
    } else {
      lines.push("*无报价档位数据*");
    }

    lines.push("", "---", "");
  }

  return lines.join("\n");
}

async function main(): Promise<void> {
  if (process.env.RPA_MOCK_MODE === "true") {
    console.error("[FAIL] RPA_MOCK_MODE=true，请关闭 Mock 后再生成真实测试数据");
    process.exit(1);
  }

  console.log("[accuracy-test] 等待全栈就绪…");
  await ensureDevStackReady();
  console.log(`[accuracy-test] 使用与用户相同的 HTTP API：${BASE}`);
  console.log(
    `[accuracy-test] 询价轮询上限 ${ACCURACY_TEST_QUOTE_TIMEOUT_MS / 1_000}s`,
  );

  const generatedAt = new Date().toISOString();
  const results: CaseResult[] = [];
  const progressPath = join(process.cwd(), ".rpa", "accuracy-test-progress.json");
  mkdirSync(join(process.cwd(), ".rpa"), { recursive: true });
  writeFileSync(
    join(process.cwd(), "docs", "测试数据.md"),
    `# 查价准确度测试数据\n\n> 生成中…\n`,
    "utf8",
  );

  for (const pair of TEST_PAIRS.filter((p) => {
    const only = process.env.ACCURACY_TEST_ONLY?.split(",").map((s) => s.trim());
    return !only?.length || only.includes(p.id);
  })) {
    const result = await runCase(pair);
    results.push(result);
    writeFileSync(progressPath, JSON.stringify({ generatedAt, results }, null, 2));
    writeFileSync(
      join(process.cwd(), "docs", "测试数据.md"),
      renderMarkdown(results, generatedAt),
      "utf8",
    );
    await sleep(15_000);
  }

  const md = renderMarkdown(results, generatedAt);
  const docPath = join(process.cwd(), "docs", "测试数据.md");
  writeFileSync(docPath, md, "utf8");

  console.log(`\n[OK] 已写入 ${docPath}`);
  console.log(`[OK] 进度 JSON：${progressPath}`);
  const done = results.filter((r) => r.status === "done").length;
  console.log(`[SUMMARY] 成功 ${done}/10`);
  if (done < 10) {
    process.exitCode = 1;
  }
}

main().catch((error) => {
  console.error("[FAIL]", error);
  process.exit(1);
});
