/**
 * 地址 commit 事件对照录证（headed 人工 vs RPA 生产路径）
 *
 * 目标：HAR + axel event trace → 对比缺失事件（不修 click/delay/retry）
 *
 * 用法：
 *   # 仅人工（headed + HAR）
 *   $env:COMPARE_MODE="manual"; $env:COMPARE_PAIR_ID="99"; npx tsx scripts/record-address-event-compare.ts
 *
 *   # 仅 RPA（fillAddressStep 生产路径）
 *   $env:COMPARE_MODE="rpa"; $env:COMPARE_PAIR_ID="99"; npx tsx scripts/record-address-event-compare.ts
 *
 *   # 先后执行人工 + RPA 并 diff
 *   $env:COMPARE_MODE="both"; npx tsx scripts/record-address-event-compare.ts
 *
 * 人工检查点（非 TTY）：
 *   $env:COMPARE_USE_FILE_TRIGGER="1"
 *   pickup 完成后: New-Item -Force .rpa\address-event-compare\trigger-pickup-done
 *   delivery 完成后: New-Item -Force .rpa\address-event-compare\trigger-delivery-done
 */
import "dotenv/config";
import { randomUUID } from "node:crypto";
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import readline from "node:readline";
import { stdin as input, stdout as output } from "node:process";
import { chromium } from "playwright";
import { loadDevEnv } from "@/lib/dev-env";
import { ensureNativeInfraIfNeeded } from "@/lib/infra/ensure-native-infra";
import {
  hostFetchMothershipCandidates,
} from "@/lib/frontend/api-client";
import { applyMothershipCandidate } from "@/lib/frontend/mothership-address";
import { getPrimaryMothershipQuoteUrl } from "@/lib/rpa/env";
import { isAnyRpaWorkerHealthy } from "@/lib/rpa/worker-health";
import { ensureRedisReady, resetRedisClient } from "@/lib/redis";
import type { QuoteRequest } from "@/modules/providers/quote-provider";
import {
  AxelEventTracer,
  diffAxelTraces,
  normalizeAxelSequence,
} from "@/scripts/lib/axel-event-tracer";
import { fillAddressStep } from "@/workers/rpa/kernel/steps/address-step";
import { RPAContext } from "@/workers/rpa/kernel/context";
import { closeBrowser, openQuotePage } from "@/workers/rpa/session-manager";
import { quotePageAdapter } from "@/workers/rpa/quote-page-adapter";
import {
  drainSelectionTraces,
  installManualSelectionListeners,
  logManualInteractionGoldSample,
  pickSelectionGoldInteractions,
  recordManualCheckpoint,
} from "@/workers/rpa/selection-trace";
import {
  stabilizeQuoteLandingPage,
  waitForQuoterWidgetHydrated,
} from "@/workers/rpa/page-prep";

loadDevEnv();

const BASE = process.env.PROVE_BASE_URL ?? "http://localhost:3000";
const TOKEN = "demo-host-token";
const CUSTOMER_ID = "CUST_001";
const OUT_ROOT = join(process.cwd(), ".rpa", "address-event-compare");
const TRIGGER_PICKUP = join(OUT_ROOT, "trigger-pickup-done");
const TRIGGER_DELIVERY = join(OUT_ROOT, "trigger-delivery-done");

const PAIRS: Record<
  string,
  {
    label: string;
    pickup: { street: string; city: string; state: string };
    delivery: { street: string; city: string; state: string };
  }
> = {
  "99": {
    label: "Key West FL → Los Angeles CA（Duval / Hollywood）",
    pickup: { street: "Duval Street", city: "Key West", state: "FL" },
    delivery: {
      street: "Hollywood Boulevard",
      city: "Los Angeles",
      state: "CA",
    },
  },
  "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" },
  },
};

const PAIR_ID = process.env.COMPARE_PAIR_ID ?? "99";
const MODE = (process.env.COMPARE_MODE ?? "both").toLowerCase();

function draftAddress(d: { street: string; city: string; state: string }) {
  const street = d.street.trim();
  const city = d.city.trim();
  const state = d.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 toQuoteRequest(
  pickup: QuoteRequest["pickup"],
  delivery: QuoteRequest["delivery"],
): QuoteRequest {
  return {
    cargoHash: "event-compare",
    pickup,
    delivery,
    weightLb: 503,
    dimsIn: { l: 48, w: 40, h: 48 },
    palletCount: 2,
    cargoType: "general_freight",
  };
}

async function fetchConfirmedAddresses(pairId: string): Promise<{
  pair: (typeof PAIRS)[string];
  pickup: QuoteRequest["pickup"];
  delivery: QuoteRequest["delivery"];
  pickupLabel: string;
  deliveryLabel: string;
}> {
  const pair = PAIRS[pairId];
  if (!pair) {
    throw new Error(`未知 COMPARE_PAIR_ID=${pairId}`);
  }

  const userPickup = draftAddress(pair.pickup);
  const userDelivery = draftAddress(pair.delivery);
  const candRes = await hostFetchMothershipCandidates(
    BASE,
    TOKEN,
    CUSTOMER_ID,
    userPickup,
    userDelivery,
  );
  if (candRes.code !== 0 || !candRes.data) {
    throw new Error(`候选 API 失败: ${candRes.message}`);
  }

  const selectedPickup = candRes.data.pickup_candidates[0];
  const selectedDelivery = candRes.data.delivery_candidates[0];
  if (!selectedPickup || !selectedDelivery) {
    throw new Error("候选不足");
  }

  const pickupApplied = applyMothershipCandidate(userPickup, selectedPickup);
  const deliveryApplied = applyMothershipCandidate(userDelivery, selectedDelivery);

  return {
    pair,
    pickupLabel: selectedPickup.display_label,
    deliveryLabel: selectedDelivery.display_label,
    pickup: {
      street: pickupApplied.street,
      city: pickupApplied.city,
      state: pickupApplied.state,
      zip: pickupApplied.zip ?? "",
      placeId: userPickup.place_id,
      formattedAddress: pickupApplied.formatted_address,
      selectedFromSuggestions: true,
      mothershipOptionId: pickupApplied.mothership_option_id ?? "",
      mothershipDisplayLabel: pickupApplied.mothership_display_label ?? "",
      selectedFromMothership: true,
    },
    delivery: {
      street: deliveryApplied.street,
      city: deliveryApplied.city,
      state: deliveryApplied.state,
      zip: deliveryApplied.zip ?? "",
      placeId: userDelivery.place_id,
      formattedAddress: deliveryApplied.formatted_address,
      selectedFromSuggestions: true,
      mothershipOptionId: deliveryApplied.mothership_option_id ?? "",
      mothershipDisplayLabel: deliveryApplied.mothership_display_label ?? "",
      selectedFromMothership: true,
    },
  };
}

async function waitForCheckpoint(label: string, triggerPath: string): Promise<void> {
  const useFile =
    process.env.COMPARE_USE_FILE_TRIGGER === "1" || !process.stdin.isTTY;

  console.log(`\n── ${label} ──`);
  if (useFile) {
    if (existsSync(triggerPath)) {
      rmSync(triggerPath, { force: true });
    }
    console.log(
      `文件触发：完成后执行\n  New-Item -Force ${triggerPath.replace(/\\/g, "\\\\")}`,
    );
    while (!existsSync(triggerPath)) {
      await new Promise((r) => setTimeout(r, 400));
    }
    rmSync(triggerPath, { force: true });
    return;
  }

  const rl = readline.createInterface({ input, output, terminal: true });
  await new Promise<void>((resolve) => {
    rl.question("完成后按 Enter 继续… ", () => {
      rl.close();
      resolve();
    });
  });
}

async function runManualSession(
  runDir: string,
  pairId: string,
): Promise<{ events: ReturnType<AxelEventTracer["getEvents"]>; harPath: string }> {
  const { pair, pickupLabel, deliveryLabel } = await fetchConfirmedAddresses(pairId);
  const entryUrl = getPrimaryMothershipQuoteUrl();
  const harPath = join(runDir, "manual.har");
  const tracer = new AxelEventTracer();

  console.log(`\n=== 人工 headed 录证：组 ${pairId} ${pair.label} ===`);
  console.log(`入口：${entryUrl}`);
  console.log(`
操作说明（金样本：必须让脚本录到「点中了哪颗 DOM」）：
  ① 提货框输入：${pair.pickup.street}, ${pair.pickup.city}, ${pair.pickup.state}
     （不要一上来打完整酒店名；与 RPA 区分，先打出街道级联想）
  ② 等联想列表出现 → 用鼠标点「${pickupLabel}」
     或键盘：↓ 移到目标 → 浏览器里 Enter（不是终端 Enter）
  ③ 确认提货锁定后，终端检查点 pickup 按 Enter
  ④ 派送框输入：${pair.delivery.street}, ${pair.delivery.city}, ${pair.delivery.state}
  ⑤ 点选「${deliveryLabel}」或 ↓+Enter
  ⑥ 终端检查点 delivery 按 Enter

终端 Enter 仅用于检查点；浏览器 Enter 才用于选联想。
录证成功标志：检查点日志出现 selectionClicks>=1 或 selectionKeys 含 Enter@INPUT
`);

  const browser = await chromium.launch({
    headless: false,
    channel: process.env.RPA_BROWSER_CHANNEL as "chrome" | "msedge" | undefined,
    slowMo: Number(process.env.RPA_SLOW_MO_MS ?? 80) || 80,
  });

  const context = await browser.newContext({
    locale: process.env.RPA_BROWSER_LOCALE?.trim() || "en-US",
    recordHar: { path: harPath, mode: "full", content: "embed" },
  });
  const page = await context.newPage();
  tracer.attach(page);
  page.setDefaultTimeout(120_000);

  tracer.setPhase("navigate");
  await page.goto(entryUrl, { waitUntil: "domcontentloaded", timeout: 90_000 });
  await page.waitForTimeout(3_000);
  await installManualSelectionListeners(page);
  console.log(
    "[manual] 已注入 click/keydown 监听（capture 阶段）；成功点选后检查点会导出 interaction 日志",
  );

  tracer.setPhase("manual-pickup");
  await waitForCheckpoint("检查点 pickup（人工点选完成）", TRIGGER_PICKUP);
  const pickupForensics = await recordManualCheckpoint(page, "manual-pickup-done");
  logManualInteractionGoldSample("pickup", pickupForensics.interactions);
  const pickupGold = pickSelectionGoldInteractions(pickupForensics.interactions);
  writeFileSync(
    join(runDir, "manual-selection-pickup.json"),
    JSON.stringify({ ...pickupForensics, gold: pickupGold }, null, 2),
    "utf8",
  );

  tracer.setPhase("manual-delivery");
  await waitForCheckpoint("检查点 delivery（人工点选完成）", TRIGGER_DELIVERY);
  const deliveryForensics = await recordManualCheckpoint(page, "manual-delivery-done");
  logManualInteractionGoldSample("delivery", deliveryForensics.interactions);
  const deliveryGold = pickSelectionGoldInteractions(deliveryForensics.interactions);
  writeFileSync(
    join(runDir, "manual-selection-delivery.json"),
    JSON.stringify({ ...deliveryForensics, gold: deliveryGold }, null, 2),
    "utf8",
  );
  writeFileSync(
    join(runDir, "manual-interactions-gold.json"),
    JSON.stringify(
      {
        pairId,
        pickup: pickupGold,
        delivery: deliveryGold,
      },
      null,
      2,
    ),
    "utf8",
  );

  await page
    .screenshot({ path: join(runDir, "manual-final.png") })
    .catch(() => undefined);

  tracer.detach();
  await context.close();
  await browser.close();

  if (!existsSync(harPath)) {
    throw new Error(`HAR 未生成: ${harPath}`);
  }

  const events = [...tracer.getEvents()];
  writeFileSync(
    join(runDir, "manual-trace.json"),
    JSON.stringify(
      {
        mode: "manual",
        pairId,
        label: pair.label,
        pickupHint: pickupLabel,
        deliveryHint: deliveryLabel,
        events,
        sequence: normalizeAxelSequence(events),
        harPath,
      },
      null,
      2,
    ),
    "utf8",
  );

  console.log(`[manual] HAR: ${harPath}`);
  console.log(`[manual] trace: ${join(runDir, "manual-trace.json")}`);
  console.log(`[manual] gold: ${join(runDir, "manual-interactions-gold.json")}`);
  console.log(`[manual] place commits:`, events.filter((e) => e.kind === "place" && e.status === 200).map((e) => e.placeId));

  return { events, harPath };
}

function writeSelectionTraceArtifact(runDir: string, mode: string): void {
  const traces = drainSelectionTraces();
  if (!traces.length) {
    return;
  }
  const outPath = join(runDir, `${mode}-selection-trace.json`);
  writeFileSync(outPath, JSON.stringify({ mode, traces }, null, 2), "utf8");
  console.log(`[${mode}] selection-trace: ${outPath} (${traces.length} 条快照)`);
}

async function runRpaSession(
  runDir: string,
  pairId: string,
): Promise<ReturnType<AxelEventTracer["getEvents"]>> {
  const { pair, pickup, delivery } = await fetchConfirmedAddresses(pairId);
  const tracer = new AxelEventTracer();

  console.log(`\n=== RPA 生产路径录证：组 ${pairId} ${pair.label} ===`);
  console.log(`fillAddressStep（与 Worker 询价相同）`);

  process.env.RPA_HEADLESS = process.env.COMPARE_RPA_HEADED === "true" ? "false" : "true";

  const opened = await openQuotePage(quotePageAdapter, false, undefined, {
    freshCargoContext: true,
  });
  const ctx = RPAContext.fromSession(opened.page, opened.context);
  tracer.attach(ctx.page);

  await stabilizeQuoteLandingPage(ctx);
  tracer.setPhase("hydrate");
  await waitForQuoterWidgetHydrated(ctx.page, 60_000);

  tracer.setPhase("rpa-fillAddress");
  try {
    await fillAddressStep(ctx, toQuoteRequest(pickup, delivery));
  } catch (error) {
    writeSelectionTraceArtifact(runDir, "rpa");
    const events = [...tracer.getEvents()];
    writeFileSync(
      join(runDir, "rpa-trace.json"),
      JSON.stringify(
        {
          mode: "rpa",
          pairId,
          label: pair.label,
          error: error instanceof Error ? error.message : String(error),
          events,
          sequence: normalizeAxelSequence(events),
        },
        null,
        2,
      ),
      "utf8",
    );
    await closeBrowser().catch(() => undefined);
    throw error;
  }

  writeSelectionTraceArtifact(runDir, "rpa");
  const events = [...tracer.getEvents()];
  writeFileSync(
    join(runDir, "rpa-trace.json"),
    JSON.stringify(
      {
        mode: "rpa",
        pairId,
        label: pair.label,
        events,
        sequence: normalizeAxelSequence(events),
      },
      null,
      2,
    ),
    "utf8",
  );

  await ctx.page
    .screenshot({ path: join(runDir, "rpa-final.png") })
    .catch(() => undefined);
  tracer.detach();
  await closeBrowser();

  console.log(`[rpa] trace: ${join(runDir, "rpa-trace.json")}`);
  console.log(`[rpa] place commits:`, events.filter((e) => e.kind === "place" && e.status === 200).map((e) => e.placeId));

  return events;
}

async function main(): Promise<void> {
  ensureNativeInfraIfNeeded();
  resetRedisClient();
  await ensureRedisReady(12, 1_000);

  const res = await fetch(BASE, { method: "GET", redirect: "manual" });
  if (res.status <= 0) {
    throw new Error("Next 未响应，请先 npm run dev:start");
  }
  if (!(await isAnyRpaWorkerHealthy())) {
    throw new Error("RPA Worker 无心跳");
  }

  const runId = randomUUID().slice(0, 8);
  const runDir = join(OUT_ROOT, `${PAIR_ID}-${runId}`);
  mkdirSync(runDir, { recursive: true });
  mkdirSync(OUT_ROOT, { recursive: true });

  console.log(`录证目录：${runDir}`);
  console.log(`COMPARE_MODE=${MODE} COMPARE_PAIR_ID=${PAIR_ID}`);

  let manualEvents: ReturnType<AxelEventTracer["getEvents"]> | null = null;
  let rpaEvents: ReturnType<AxelEventTracer["getEvents"]> | null = null;

  if (MODE === "manual" || MODE === "both") {
    const manual = await runManualSession(runDir, PAIR_ID);
    manualEvents = [...manual.events];
  }

  if (MODE === "rpa" || MODE === "both") {
    try {
      rpaEvents = [...(await runRpaSession(runDir, PAIR_ID))];
    } catch (error) {
      console.error("[rpa] fillAddress 失败（trace 已写入）:", error);
      if (MODE === "rpa") {
        process.exitCode = 1;
      }
    }
  }

  if (manualEvents && rpaEvents) {
    const diff = diffAxelTraces(manualEvents, rpaEvents);
    const comparePath = join(runDir, "compare.json");
    writeFileSync(
      comparePath,
      JSON.stringify(
        {
          runId,
          pairId: PAIR_ID,
          finishedAt: new Date().toISOString(),
          ...diff,
          interpretation: interpretDiff(diff),
        },
        null,
        2,
      ),
      "utf8",
    );
    console.log(`\n对照报告：${comparePath}`);
    console.log(JSON.stringify(interpretDiff(diff), null, 2));
  }

  const summaryPath = join(runDir, "README.txt");
  writeFileSync(
    summaryPath,
    [
      `runId=${runId}`,
      `pair=${PAIR_ID}`,
      `mode=${MODE}`,
      "",
      "产物：",
      "- manual.har + manual-trace.json（人工）",
      "- rpa-trace.json（RPA fillAddressStep）",
      "- compare.json（both 模式）",
      "",
      "下一步：仅实现 compare.json 中 missingInRpa 对应的事件，禁止 patch click/delay/retry。",
    ].join("\n"),
    "utf8",
  );
}

function interpretDiff(
  diff: ReturnType<typeof diffAxelTraces>,
): Record<string, unknown> {
  return {
    verdict: diff.verdict,
    manualPlaceCommits: diff.manualPlaceCommits,
    rpaPlaceCommits: diff.rpaPlaceCommits,
    missingInRpaCount: diff.missingInRpa.length,
    missingInRpa: diff.missingInRpa.slice(0, 20),
    extraInRpa: diff.extraInRpa.slice(0, 20),
    nextAction:
      diff.verdict === "RPA_MISSING_PLACE_COMMIT"
        ? "RPA 未触发 GET place/{id}；对照 manual trace 中 pickup/delivery 的 place 事件，补缺失 selection 事件"
        : diff.verdict === "MANUAL_NO_PLACE_COMMIT"
          ? "人工录证未捕获 place commit，请重跑 manual 并确认真实点选联想项"
          : "查看 missingInRpa 逐行 diff",
  };
}

main().catch((error) => {
  console.error(error);
  process.exit(1);
});
