/**
 * Priority1 可视全链回放 — 供 worker 生产与本地脚本共用
 */
import { randomUUID } from "node:crypto";
import { mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { chromium, type Locator, type Page } from "playwright";
import { launchRpaBrowser } from "@/lib/rpa/browser-launch";
import { createRpaBrowserContext } from "@/lib/rpa/browser-context";
import {
  formatExternalSiteNavigationError,
  gotoWithResilience,
  isTransientNavigationError,
} from "@/lib/rpa/page-goto";
import { shouldKeepBrowserOpen } from "@/lib/rpa/env";
import {
  extractPriority1VisibleQuotes,
  formatPriority1QuoteOutcomeMessage,
  formatPriority1QuotesTable,
  isPriority1ManualFollowupPage,
  PRIORITY1_MANUAL_FOLLOWUP_MESSAGE,
  resolvePriority1QuoteBundles,
  resolvePriority1SubmitOutcome,
  type Priority1QuoteLine,
  type Priority1QuoteOutcome,
} from "@/workers/rpa/priority1/quote-extract";

import {
  parsePriority1DemoFromEnv,
  PRIORITY1_CANONICAL_PATHS,
  type Priority1DemoInput,
} from "@/workers/rpa/priority1/demo-input";
import {
  EXPEDITED_TRAILER,
  FTL_ADDITIONAL,
  FTL_TRAILER,
  expeditedLiftgateAvailable,
  getShipmentConfig,
  type ShipmentTypeConfig,
} from "@/workers/rpa/priority1/shipment-types";
import {
  fillTemperatureRange,
  forceFeatheryCheckbox,
  freightClassOptionMatches,
  openDeckSelectionOk,
  readSelectVisibleText,
  selectExpeditedLiftgate,
  selectFreightClass,
  selectFtlAdditionalService,
  selectOpenDeckSubtype,
  toggleLtlAccessorial,
  waitForFeatheryCheckbox,
} from "@/workers/rpa/priority1/form-interactions";
import {
  drainHumanAssistEvents,
  formatHumanEventsAsPlaywright,
  installHumanAssistRecorder,
  type HumanAssistEvent,
  type HumanAccessorialRecording,
} from "@/workers/rpa/priority1/human-assist-recorder";
import {
  P08_LTL_ACCESSORIAL_LABELS,
} from "@/workers/rpa/priority1/demo-input";

const ENTRY_URL =
  "https://www.priority1.com/instant-freight-quotes/#New%20Step%201";

function envHeadless(): boolean {
  return process.env.RPA_HEADLESS === "true";
}
function envDwellMs(): number {
  if (process.env.RPA_DWELL_MS != null) {
    return Number(process.env.RPA_DWELL_MS);
  }
  return envHeadless() ? 0 : 2_000;
}
function envSlowMo(): number | undefined {
  if (process.env.RPA_SLOW_MO_MS != null) {
    const n = Number(process.env.RPA_SLOW_MO_MS);
    return n > 0 ? n : undefined;
  }
  return envHeadless() ? undefined : 150;
}
const OBSERVE_POLL_MS = Number(process.env.RPA_OBSERVE_POLL_MS ?? 400);
const OBSERVE_MAX_MS = Number(process.env.RPA_OBSERVE_MAX_MS ?? 20_000);

const QUOTE_WAIT_MS = Number(process.env.RPA_QUOTE_WAIT_MS ?? 90_000);
const QUOTE_POLL_MS = Number(process.env.RPA_QUOTE_POLL_MS ?? 800);
const FEATHERY_CLICK = { noWaitAfter: true as const };

function shipmentCheckboxSelectors(cfg: ShipmentTypeConfig): string[] {
  if (cfg.id === "ftl") return ["#cb_st_ftl", "#cb_st_ftl_sel"];
  if (cfg.id === "expedited") return ["#cb_st_exp"];
  return [cfg.checkboxSelector];
}

async function isShipmentCardSelected(
  page: Page,
  cfg: ShipmentTypeConfig,
): Promise<boolean> {
  return page.evaluate(
    function (args) {
      for (let s = 0; s < args.selectors.length; s += 1) {
        const cb = document.querySelector(
          `#container-mh ${args.selectors[s]}`,
        ) as HTMLInputElement | null;
        if (cb?.checked) return true;
      }

      const pattern = new RegExp(args.cardTextSource, "i");
      const nodes = document.querySelectorAll("#container-mh *");
      for (let i = 0; i < nodes.length; i += 1) {
        const el = nodes[i];
        const text = (el.textContent || "").trim();
        if (!pattern.test(text) || text.length > 120) continue;
        let cur: Element | null = el;
        for (let d = 0; d < 5 && cur; d += 1, cur = cur.parentElement) {
          const bg = getComputedStyle(cur).backgroundColor;
          if (bg === "rgb(0, 0, 0)" || bg === "black") return true;
        }
      }
      return false;
    },
    {
      selectors: shipmentCheckboxSelectors(cfg),
      cardTextSource: cfg.cardText.source,
    },
  );
}

async function fillPhoneUs(ctx: VisualCtx, locator: Locator): Promise<void> {
  await locator.scrollIntoViewIfNeeded();
  await locator.click(FEATHERY_CLICK);
  await ctx.page.waitForTimeout(400);

  const countryBtn = ctx.page.locator(
    ".iti__selected-country, .iti__selected-flag, [aria-label*='country' i]",
  );
  if ((await countryBtn.count()) > 0) {
    await countryBtn.first().click(FEATHERY_CLICK).catch(() => undefined);
    await ctx.page.waitForTimeout(300);
    await ctx.page
      .locator(".iti__country-list, [role='listbox']")
      .getByText(/United States|USA/i)
      .first()
      .click(FEATHERY_CLICK)
      .catch(() => undefined);
    await ctx.page.waitForTimeout(300);
  }

  await locator.fill("");
  await locator.pressSequentially(ctx.demo.phone, {
    delay: envHeadless() ? 0 : 80,
  });
  await locator.evaluate((el) => {
    el.dispatchEvent(new Event("input", { bubbles: true }));
    el.dispatchEvent(new Event("change", { bubbles: true }));
    el.dispatchEvent(new Event("blur", { bubbles: true }));
  });
}

type Observation = {
  step: string;
  label: string;
  expected: string;
  observed: string;
  ok: boolean;
  screenshot: string;
  ts: number;
};

type VisualCtx = {
  page: Page;
  outDir: string;
  demo: Priority1DemoInput;
  observations: Observation[];
  snapSeq: number;
  networkBodies: string[];
  quotes: Priority1QuoteLine[];
  workerFast: boolean;
};

function attachPriority1QuoteNetworkCapture(
  page: Page,
  sink: string[],
): void {
  page.on("response", async (res) => {
    const url = res.url();
    const isFeatheryQuote =
      /feathery\.io/i.test(url) &&
      /custom_request|panel\/custom|submit|form\/submit/i.test(url);
    const isPriority1Api =
      /priority1\.com/i.test(url) && /quote|rate|pricing|ltl/i.test(url);
    if (!isFeatheryQuote && !isPriority1Api) {
      return;
    }
    try {
      const body = await res.text();
      if (body.length > 20) sink.push(body);
    } catch {
      /* ignore */
    }
  });
}

type Priority1QuoteWaitResult = {
  quotes: Priority1QuoteLine[];
  outcome: Priority1QuoteOutcome;
  message?: string;
};

async function extractAndSaveQuotes(
  ctx: VisualCtx,
  outcome: Priority1QuoteOutcome = "instant_quote",
  message?: string,
): Promise<Priority1QuoteLine[]> {
  const domText = await ctx.page.locator("body").innerText();
  const bundles = resolvePriority1QuoteBundles(ctx.networkBodies, domText);
  const quotes = bundles.visible;
  ctx.quotes = quotes;

  const path = join(ctx.outDir, "quotes.json");
  writeFileSync(
    path,
    `${JSON.stringify(
      {
        count: quotes.length,
        outcome,
        message: message ?? null,
        note: "visible=页面可见报价卡片；allApi=后台返回的全量承运商（仅供调试）",
        quotes,
        allApi: bundles.allApi,
      },
      null,
      2,
    )}\n`,
    "utf8",
  );

  console.log("\n════════ 提取报价（页面可见）════════");
  if (outcome === "manual_followup") {
    console.log(formatPriority1QuoteOutcomeMessage(outcome, message));
  } else {
    console.log(formatPriority1QuotesTable(quotes));
  }
  if (bundles.allApi.length > quotes.length) {
    console.log(
      `（后台另有 ${bundles.allApi.length} 家承运商报价，未在页面展示，见 quotes.json → allApi）`,
    );
  }
  console.log(`报价 JSON: ${path}`);
  return quotes;
}

function digits(s: string): string {
  return s.replace(/\D/g, "");
}

function normalizeNumeric(s: string): string {
  return s.replace(/,/g, "").trim();
}

function valueMatches(
  observed: string,
  expected: string,
  mode: "exact" | "contains" | "digits",
): boolean {
  if (mode === "exact") {
    if (observed === expected) return true;
    return normalizeNumeric(observed) === normalizeNumeric(expected);
  }
  if (mode === "contains") return observed.includes(expected);
  return digits(observed).includes(digits(expected));
}

async function dwell(page: Page, label: string, workerFast = false): Promise<void> {
  if (workerFast || envDwellMs() <= 0) return;
  console.log(`[观测] 停留 ${envDwellMs()}ms — ${label}`);
  await page.waitForTimeout(envDwellMs());
}

async function highlight(locator: Locator): Promise<void> {
  await locator.evaluate((el) => {
    el.style.outline = "3px solid #f5c518";
    el.style.outlineOffset = "2px";
  }).catch(() => undefined);
}

async function snapLocator(
  ctx: VisualCtx,
  locator: Locator,
  slug: string,
): Promise<string> {
  if (ctx.workerFast) return "";
  const n = String(++ctx.snapSeq).padStart(2, "0");
  const path = join(ctx.outDir, `${n}-${slug}.png`);
  await locator.screenshot({ path });
  return path;
}

/** 轮询直到可见输入框出现期望值，期间对输入框截图 */
async function observeInput(
  ctx: VisualCtx,
  step: string,
  label: string,
  locator: Locator,
  expected: string,
  mode: "exact" | "contains" | "digits" = "exact",
): Promise<void> {
  await locator.waitFor({ state: "visible" });
  await locator.scrollIntoViewIfNeeded();
  if (!ctx.workerFast) {
    await highlight(locator);
  }

  const started = Date.now();
  let observed = "";
  let shot = "";
  const observeMaxMs = ctx.workerFast
    ? Math.min(OBSERVE_MAX_MS, 6_000)
    : OBSERVE_MAX_MS;
  const observePollMs = ctx.workerFast ? 120 : OBSERVE_POLL_MS;

  while (Date.now() - started < observeMaxMs) {
    observed = await locator.inputValue().catch(() => "");
    if (!ctx.workerFast) {
      shot = await snapLocator(ctx, locator, `${slugify(label)}-observe`);
    }
    if (valueMatches(observed, expected, mode)) {
      break;
    }
    await ctx.page.waitForTimeout(observePollMs);
  }

  const ok = valueMatches(observed, expected, mode);
  ctx.observations.push({
    step,
    label,
    expected,
    observed: observed || "(空)",
    ok,
    screenshot: shot,
    ts: Date.now(),
  });

  if (!ctx.workerFast) {
    console.log(
      `[观测] ${ok ? "✓" : "✗"} ${label} → 输入框可见值: "${observed || "(空)"}" (期望 ${mode}: "${expected}")`,
    );
    if (shot) {
      console.log(`[观测]   截图: ${shot}`);
    }
  }

  if (!ok) {
    throw new Error(
      `${label} 输入框未出现期望值（可见值 "${observed || "(空)"}"）— 请查看截图`,
    );
  }
  await dwell(ctx.page, `${label} 已写入输入框`, ctx.workerFast);
}

async function typeIntoField(
  ctx: VisualCtx,
  locator: Locator,
  value: string,
): Promise<void> {
  await locator.scrollIntoViewIfNeeded();
  await locator.click(FEATHERY_CLICK);
  await locator.fill("");
  await locator.pressSequentially(value, { delay: envHeadless() ? 0 : 60 });
  await locator.evaluate((el, v) => {
    const input = el as HTMLInputElement;
    const setter = Object.getOwnPropertyDescriptor(
      HTMLInputElement.prototype,
      "value",
    )?.set;
    setter?.call(input, v);
    input.dispatchEvent(new Event("input", { bubbles: true }));
    input.dispatchEvent(new Event("change", { bubbles: true }));
    input.dispatchEvent(new Event("blur", { bubbles: true }));
  }, value);
}

async function observeSelect(
  ctx: VisualCtx,
  step: string,
  label: string,
  locator: Locator,
  expected: string,
): Promise<void> {
  await locator.waitFor({ state: "visible" });
  await locator.scrollIntoViewIfNeeded();
  await highlight(locator);
  const shot = await snapLocator(ctx, locator, `${slugify(label)}-observe`);
  const observed = await readSelectVisibleText(locator);
  const ok =
    observed === expected ||
    observed.includes(expected) ||
    expected.includes(observed) ||
    (label === "Freight Class" && freightClassOptionMatches(observed, expected));
  ctx.observations.push({
    step,
    label,
    expected,
    observed: observed || "(空)",
    ok,
    screenshot: shot,
    ts: Date.now(),
  });
  console.log(`[观测] ${ok ? "✓" : "✗"} ${label} → 下拉可见: "${observed}"`);
  console.log(`[观测]   截图: ${shot}`);
  if (!ok) {
    throw new Error(`${label} 下拉未选中期望值`);
  }
  await dwell(ctx.page, `${label} 已选中`);
}

async function observeShipmentType(ctx: VisualCtx): Promise<void> {
  const cfg = getShipmentConfig(ctx.demo.shipmentType);
  const area = ctx.page
    .locator("#container-mh")
    .filter({ hasText: /Shipment Type/i })
    .first();
  await area.scrollIntoViewIfNeeded().catch(() => undefined);
  await snapLocator(ctx, area, "shipment-type-before");

  for (let attempt = 0; attempt < 3; attempt += 1) {
    if (await isShipmentCardSelected(ctx.page, cfg)) break;
    for (const sel of shipmentCheckboxSelectors(cfg)) {
      await forceFeatheryCheckbox(ctx.page, sel);
      const cb = ctx.page.locator(`#container-mh ${sel}`).first();
      if ((await cb.count()) > 0) {
        await cb.click({ force: true, ...FEATHERY_CLICK }).catch(() => undefined);
      }
    }
    await ctx.page.waitForTimeout(500);
    await ctx.page
      .locator("#container-mh")
      .getByText(cfg.cardText)
      .first()
      .click(FEATHERY_CLICK)
      .catch(() => undefined);
    await ctx.page.waitForTimeout(600);
  }

  const ok = await isShipmentCardSelected(ctx.page, cfg);
  const shot = await snapLocator(ctx, area, "shipment-type-after");
  ctx.observations.push({
    step: "step1",
    label: `Shipment Type (${cfg.id})`,
    expected: `${cfg.displayName} selected`,
    observed: ok ? "selected" : "not selected",
    ok,
    screenshot: shot,
    ts: Date.now(),
  });
  console.log(
    `[观测] ${ok ? "✓" : "✗"} Shipment Type → ${cfg.displayName}${ok ? " 已选中" : " 未选中"}`,
  );
  if (!ok) {
    throw new Error(`Shipment Type ${cfg.id} 未选中`);
  }
  await dwell(ctx.page, "Shipment Type 已选中");
}

async function isFeatheryCardSelected(page: Page, label: string): Promise<boolean> {
  return page.evaluate(function (labelText) {
    const nodes = document.querySelectorAll("#container-mh *");
    for (let i = 0; i < nodes.length; i += 1) {
      const el = nodes[i];
      const text = (el.textContent || "").trim();
      if (!text.startsWith(labelText) || text.length > 220) continue;
      let cur: Element | null = el;
      for (let d = 0; d < 6 && cur; d += 1, cur = cur.parentElement) {
        const bg = getComputedStyle(cur).backgroundColor;
        if (bg === "rgb(0, 0, 0)" || bg === "black") return true;
      }
    }
    return false;
  }, label);
}

/** 点击 Feathery 卡片（隐藏 checkbox + 黑底高亮观测） */
async function selectFeatheryCard(
  ctx: VisualCtx,
  step: string,
  label: string,
  selector: string,
  sectionHint?: RegExp,
): Promise<void> {
  const root = ctx.page.locator("#container-mh");
  if (sectionHint) {
    await root
      .filter({ hasText: sectionHint })
      .first()
      .scrollIntoViewIfNeeded()
      .catch(() => undefined);
  }

  const cb = root.locator(selector).first();
  await cb.scrollIntoViewIfNeeded().catch(() => undefined);
  const labelRe = new RegExp(
    `^${label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
    "i",
  );

  for (let attempt = 0; attempt < 4; attempt += 1) {
    if ((await cb.count()) > 0) {
      await forceFeatheryCheckbox(ctx.page, selector);
      await cb.click({ force: true, ...FEATHERY_CLICK });
    }
    await root
      .getByText(labelRe)
      .first()
      .click(FEATHERY_CLICK)
      .catch(() => undefined);
    await ctx.page.waitForTimeout(600);

    const checked =
      (await cb.count()) > 0 ? await cb.isChecked().catch(() => false) : false;
    const visual = await isFeatheryCardSelected(ctx.page, label);
    if (checked || visual) {
      const shot = await snapLocator(ctx, root, slugify(label));
      ctx.observations.push({
        step,
        label,
        expected: "selected (dark card)",
        observed: checked ? "checked" : "visual selected",
        ok: true,
        screenshot: shot,
        ts: Date.now(),
      });
      console.log(`[观测] ✓ ${label} → 卡片已选中（黑底高亮）`);
      await dwell(ctx.page, `${label} 已选中`);
      return;
    }
  }

  throw new Error(`${label} 卡片未选中 — 请查看 Step 2 截图`);
}

async function fillFtlTrailerConditionals(ctx: VisualCtx, step: string): Promise<void> {
  const demo = ctx.demo;

  if (demo.ftlTrailer === "open_deck") {
    const { locator: sel, selectedText } = await selectOpenDeckSubtype(
      ctx.page,
      demo.openDeckSubtype,
    );
    await sel.scrollIntoViewIfNeeded();
    await highlight(sel);
    const shot = await snapLocator(ctx, sel, "Open-Deck-Type-observe");
    const observed = (await readSelectVisibleText(sel).catch(() => "")) || selectedText;
    const ok = openDeckSelectionOk(observed, demo.openDeckSubtype);
    ctx.observations.push({
      step,
      label: "Open Deck Type",
      expected: demo.openDeckSubtype,
      observed: observed || demo.openDeckSubtype,
      ok,
      screenshot: shot,
      ts: Date.now(),
    });
    console.log(
      `[观测] ${ok ? "✓" : "✗"} Open Deck Type → ${observed || demo.openDeckSubtype}`,
    );
    if (!ok) {
      throw new Error("Open Deck Type 未选中 — 见 form-logic-map / form-interactions");
    }
    await dwell(ctx.page, "Open Deck Type 已选中");
    return;
  }

  if (demo.ftlTrailer === "temperature_controlled") {
    const { min: minLoc, max: maxLoc } = await fillTemperatureRange(
      ctx.page,
      demo.tempMinF,
      demo.tempMaxF,
    );
    await observeInput(ctx, step, "Minimum Temperature", minLoc, demo.tempMinF);
    await observeInput(ctx, step, "Maximum Temperature", maxLoc, demo.tempMaxF);
    return;
  }

  if (demo.ftlTrailer === "ftl_expedited") {
    const sub = EXPEDITED_TRAILER[demo.ftlExpeditedTrailer];
    await ctx.page
      .locator("#container-mh")
      .getByText(/Select Expedited Trailer Type/i)
      .first()
      .waitFor({ state: "visible", timeout: 20_000 })
      .catch(() => undefined);
    await ctx.page.waitForTimeout(1500);
    await waitForFeatheryCheckbox(ctx.page, sub.selector, 15_000).catch(() => undefined);
    await selectFeatheryCard(
      ctx,
      step,
      sub.label,
      sub.selector,
      /Select Expedited Trailer Type/i,
    );
    if (
      demo.expeditedLiftgate &&
      expeditedLiftgateAvailable(demo.ftlExpeditedTrailer)
    ) {
      await selectExpeditedLiftgate(ctx.page);
    }
  }
}

function slugify(s: string): string {
  return s.replace(/[^\w]+/g, "-").replace(/^-|-$/g, "").slice(0, 40);
}

function formLocator(ctx: VisualCtx, selector: string): Locator {
  return ctx.page.locator(`#container-mh ${selector}`).first();
}

async function fillStep1(ctx: VisualCtx): Promise<void> {
  const step = "step1";
  await observeShipmentType(ctx);

  const origin = formLocator(ctx, "#origin-zip");
  await typeIntoField(ctx, origin, ctx.demo.originZip);
  await observeInput(ctx, step, "Pickup ZIP Code", origin, ctx.demo.originZip);

  const dest = formLocator(ctx, "#destination-zip");
  await typeIntoField(ctx, dest, ctx.demo.destinationZip);
  await observeInput(ctx, step, "Delivery ZIP Code", dest, ctx.demo.destinationZip);

  const freq = formLocator(ctx, "#freight-shipment");
  await freq.selectOption(ctx.demo.shipmentFrequency);
  await observeSelect(ctx, step, "Shipments Per Month", freq, ctx.demo.shipmentFrequency);

  const date = formLocator(ctx, "#pickup-date");
  await typeIntoField(ctx, date, ctx.demo.pickupDate);
  await observeInput(ctx, step, "Pickup Date", date, ctx.demo.pickupDate);

  const email = formLocator(ctx, "#business-email");
  await typeIntoField(ctx, email, ctx.demo.email);
  await observeInput(ctx, step, "Business Email", email, ctx.demo.email);

  const phone = formLocator(ctx, "#client-phone");
  await fillPhoneUs(ctx, phone);
  await observeInput(ctx, step, "Phone", phone, ctx.demo.phone, "digits");
}

async function waitForStep2Ready(
  ctx: VisualCtx,
  timeoutMs: number,
): Promise<boolean> {
  const cfg = getShipmentConfig(ctx.demo.shipmentType);
  const ready = formLocator(ctx, cfg.step2ReadySelector);
  const started = Date.now();
  while (Date.now() - started < timeoutMs) {
    if (await ready.isVisible().catch(() => false)) {
      await highlight(ready);
      await snapLocator(ctx, ready, "step2-ready-visible");
      console.log(`[观测] ✓ Step 2 ${cfg.step2ReadySelector} 已出现`);
      await dwell(ctx.page, "已进入 Step 2");
      return true;
    }
    await ctx.page.waitForTimeout(OBSERVE_POLL_MS);
  }
  return false;
}

async function submitStep1(ctx: VisualCtx): Promise<void> {
  const cfg = getShipmentConfig(ctx.demo.shipmentType);
  await ctx.page
    .locator("#container-mh")
    .click({ position: { x: 10, y: 10 } })
    .catch(() => undefined);
  await ctx.page.waitForTimeout(400);

  const btn = ctx.page
    .locator("#container-mh")
    .getByRole("button", { name: /GET INSTANT QUOTES/i })
    .first();
  await highlight(btn);
  await snapLocator(ctx, btn, "step1-submit-btn");
  await btn.click(FEATHERY_CLICK);
  await ctx.page.waitForTimeout(1_500);
  await btn.click(FEATHERY_CLICK).catch(() => undefined);

  if (await waitForStep2Ready(ctx, 30_000)) {
    return;
  }

  console.log("[观测] ⚠ 按钮提交未进入 Step 2，使用类型哈希回退");
  await gotoWithResilience(
    ctx.page,
    `https://www.priority1.com/instant-freight-quotes/${cfg.step2HashPath}`,
  );
  await ctx.page.waitForTimeout(1500);

  if (!(await waitForStep2Ready(ctx, 30_000))) {
    throw new Error(`Step 2 ${cfg.step2ReadySelector} 未出现`);
  }
}

async function fillStep2Ltl(
  ctx: VisualCtx,
  opts: { skipAccessorials?: boolean } = {},
): Promise<void> {
  const step = "step2";
  const fields: Array<{ label: string; sel: string; val: string }> = [
    { label: "Length", sel: "#length", val: ctx.demo.lengthIn },
    { label: "Width", sel: "#width", val: ctx.demo.widthIn },
    { label: "Height", sel: "#height", val: ctx.demo.heightIn },
    { label: "Total Weight", sel: "#weight", val: ctx.demo.weightLb },
    { label: "Number Of Pallets", sel: "#number-of-pallets", val: ctx.demo.palletCount },
  ];

  for (const f of fields) {
    const loc = formLocator(ctx, f.sel);
    await typeIntoField(ctx, loc, f.val);
    await observeInput(ctx, step, f.label, loc, f.val);
  }

  const packaging = formLocator(ctx, "#freight-package, #packaging-type, #packaging").filter({
    has: ctx.page.locator('option:has-text("Pallet")'),
  });
  if ((await packaging.count()) > 0) {
    await packaging.first().selectOption({ label: ctx.demo.packaging }).catch(() =>
      packaging.first().selectOption(ctx.demo.packaging),
    );
    await observeSelect(ctx, step, "Packaging Type", packaging.first(), ctx.demo.packaging);
  }

  const freightClass = formLocator(ctx, "#freight-class, #freight-type");
  if (await freightClass.count()) {
    await ctx.page.waitForTimeout(300);
    const fc = await selectFreightClass(ctx.page, ctx.demo.freightClass);
    await observeSelect(ctx, step, "Freight Class", fc, ctx.demo.freightClass);
  }

  const pickupLoc = formLocator(ctx, "#PickupLocation");
  if (await pickupLoc.count()) {
    await pickupLoc.selectOption(ctx.demo.pickupLocation);
    await observeSelect(ctx, step, "Pickup Location Type", pickupLoc, ctx.demo.pickupLocation);
  }

  const dropLoc = formLocator(ctx, "#DropoffLocation");
  if (await dropLoc.count()) {
    await dropLoc.selectOption(ctx.demo.deliveryLocation);
    await observeSelect(ctx, step, "Delivery Location Type", dropLoc, ctx.demo.deliveryLocation);
  }

  if (!opts.skipAccessorials) {
    await ctx.page.waitForTimeout(600);
    for (const label of [...ctx.demo.pickupServices, ...ctx.demo.deliveryServices]) {
      const checked = await toggleLtlAccessorial(ctx.page, label);
      const root = ctx.page.locator("#container-mh");
      const shot = await snapLocator(ctx, root, slugify(label)).catch(() =>
        join(ctx.outDir, `${slugify(label)}.png`),
      );
      ctx.observations.push({
        step,
        label,
        expected: "checked",
        observed: checked ? "checked" : "unchecked",
        ok: checked,
        screenshot: shot,
        ts: Date.now(),
      });
      console.log(`[观测] ${checked ? "✓" : "✗"} ${label} → ${checked ? "已勾选" : "未勾选"}`);
    }
  }
}

async function fillStep2Ftl(
  ctx: VisualCtx,
  soft = false,
  opts: { skipAdditionalServices?: boolean; stopAfterTrailer?: boolean } = {},
): Promise<void> {
  const step = "step2";
  const trailer = FTL_TRAILER[ctx.demo.ftlTrailer];

  const runSoft = async (label: string, fn: () => Promise<void>) => {
    try {
      await fn();
    } catch (e) {
      const msg = e instanceof Error ? e.message : String(e);
      console.log(`[human-assist] ⚠ 自动步骤未成功: ${label} — ${msg}`);
      if (!soft) throw e;
    }
  };

  await runSoft(`拖车 ${trailer.label}`, () =>
    selectFeatheryCard(ctx, step, trailer.label, trailer.selector, /Select Trailer Type/i),
  );
  await runSoft("拖车条件字段", () => fillFtlTrailerConditionals(ctx, step));

  if (opts.stopAfterTrailer) {
    return;
  }

  if (!opts.skipAdditionalServices) {
    for (const svc of ctx.demo.ftlAdditionalServices) {
      const meta = FTL_ADDITIONAL[svc];
      await runSoft(`附加服务 ${meta.label}`, async () => {
        const ok = await selectFtlAdditionalService(ctx.page, meta.selector, meta.label);
        const shot = await snapLocator(ctx, ctx.page.locator("#container-mh"), slugify(meta.label));
        ctx.observations.push({
          step,
          label: meta.label,
          expected: "selected (Additional Services)",
          observed: ok ? "checked" : "unchecked",
          ok,
          screenshot: shot,
          ts: Date.now(),
        });
        console.log(`[观测] ${ok ? "✓" : "✗"} ${meta.label} → ${ok ? "附加服务已勾选" : "未勾选"}`);
        if (!ok) throw new Error(`${meta.label} 未勾选`);
        await dwell(ctx.page, `${meta.label} 已选中`);
      });
    }
  }

  await runSoft("What are you shipping?", async () => {
    const commodity = formLocator(ctx, "#Commodity");
    await typeIntoField(ctx, commodity, ctx.demo.commodity);
    await observeInput(ctx, step, "What are you shipping?", commodity, ctx.demo.commodity);
  });

  await runSoft("Total Weight", async () => {
    const weight = formLocator(ctx, "#weight");
    await typeIntoField(ctx, weight, ctx.demo.weightLb);
    await observeInput(ctx, step, "Total Weight", weight, ctx.demo.weightLb);
  });

  if (ctx.demo.ftlTrailer !== "ftl_expedited") {
    await runSoft("Number Of Pallets", async () => {
      const pallets = formLocator(ctx, "#Pallets");
      if (!(await pallets.count())) return;
      await typeIntoField(ctx, pallets, ctx.demo.palletCount);
      await observeInput(ctx, step, "Number Of Pallets", pallets, ctx.demo.palletCount);
    });
  }
}

async function fillStep2Expedited(ctx: VisualCtx): Promise<void> {
  const step = "step2";
  const demo = ctx.demo;
  const trailer = EXPEDITED_TRAILER[demo.expeditedTrailer];
  await selectFeatheryCard(
    ctx,
    step,
    trailer.label,
    trailer.selector,
    /Select Trailer Type|Sprinter Van/i,
  );

  if (demo.expeditedLiftgate && expeditedLiftgateAvailable(demo.expeditedTrailer)) {
    await selectExpeditedLiftgate(ctx.page);
  }

  const commodity = formLocator(ctx, "#Commodity");
  await typeIntoField(ctx, commodity, ctx.demo.commodity);
  await observeInput(ctx, step, "Commodity", commodity, ctx.demo.commodity);

  const weight = formLocator(ctx, "#weight");
  await typeIntoField(ctx, weight, ctx.demo.weightLb);
  await observeInput(ctx, step, "Total Weight", weight, ctx.demo.weightLb);
}

async function fillStep2(
  ctx: VisualCtx,
  soft = false,
  stepOpts: { skipAccessorials?: boolean; skipFtlAdditionalServices?: boolean } = {},
): Promise<void> {
  if (ctx.demo.shipmentType === "ftl") {
    await fillStep2Ftl(ctx, soft, {
      skipAdditionalServices: stepOpts.skipFtlAdditionalServices,
      stopAfterTrailer: stepOpts.skipFtlAdditionalServices,
    });
    return;
  }
  if (ctx.demo.shipmentType === "expedited") {
    await fillStep2Expedited(ctx);
    return;
  }
  await fillStep2Ltl(ctx, stepOpts);
}

async function waitForQuotes(ctx: VisualCtx): Promise<Priority1QuoteWaitResult> {
  const pollMs = ctx.workerFast ? Math.min(QUOTE_POLL_MS, 600) : QUOTE_POLL_MS;
  console.log(`[观测] 等待报价加载（最长 ${QUOTE_WAIT_MS / 1000}s，轮询 ${pollMs}ms）…`);
  const started = Date.now();
  while (Date.now() - started < QUOTE_WAIT_MS) {
    const domText = await ctx.page.locator("body").innerText().catch(() => "");
    if (isPriority1ManualFollowupPage(domText)) break;
    const visible = extractPriority1VisibleQuotes(ctx.networkBodies, domText);
    if (visible.length > 0) break;
    await ctx.page.waitForTimeout(pollMs);
  }
  if (!ctx.workerFast) {
    await ctx.page.screenshot({
      path: join(ctx.outDir, "quote-result-full.png"),
      fullPage: true,
    });
  }
  const domText = await ctx.page.locator("body").innerText().catch(() => "");
  const resolved = resolvePriority1SubmitOutcome(ctx.networkBodies, domText);
  await extractAndSaveQuotes(ctx, resolved.outcome, resolved.message);
  return resolved;
}

function printP08AccessorialHandoff(): void {
  console.log("\n════════ P08 人工录制 — 请勾选全部 11 项附加服务 ════════");
  console.log("[record-p08] 脚本已自动完成 Step1 + Step2（至地点类型），附加服务留空。");
  console.log("[record-p08] 请在浏览器中依次勾选：\n");
  console.log("  提货 Pickup Services（4 项）：");
  for (let i = 0; i < 4; i += 1) {
    console.log(`    ${i + 1}. ${P08_LTL_ACCESSORIAL_LABELS[i]}`);
  }
  console.log("\n  派送 Delivery Services（7 项）：");
  for (let i = 4; i < P08_LTL_ACCESSORIAL_LABELS.length; i += 1) {
    console.log(`    ${i + 1}. ${P08_LTL_ACCESSORIAL_LABELS[i]}`);
  }
  console.log("\n[record-p08] 全部勾选后点击 GET INSTANT QUOTES；脚本记录操作直至报价出现。\n");
}

function printFtlAdditionalHandoff(demo: Priority1DemoInput): void {
  console.log("\n════════ FTL 人工录制 — 请完成 Step 2 剩余项 ════════");
  console.log("[record-ftl] 脚本已自动完成 Step1 + 拖车类型（及 Open Deck/温控等条件字段）。");
  console.log("[record-ftl] Additional Services 及后续字段留空，请人工操作：\n");
  let n = 1;
  for (const svc of demo.ftlAdditionalServices) {
    const meta = FTL_ADDITIONAL[svc];
    console.log(`  ${n}. 勾选 Additional Services → ${meta.label}`);
    n += 1;
  }
  console.log(`  ${n}. What are you shipping? → ${demo.commodity}`);
  n += 1;
  console.log(`  ${n}. Total Weight → ${demo.weightLb} lbs`);
  n += 1;
  if (demo.ftlTrailer !== "ftl_expedited") {
    console.log(`  ${n}. Number Of Pallets → ${demo.palletCount}`);
    n += 1;
  }
  console.log(`  ${n}. 点击 GET INSTANT QUOTES 提交`);
  console.log("\n[record-ftl] 操作会被持续记录；出现报价后自动保存，或 Ctrl+C 结束保存。\n");
}

function printHumanAssistHandoff(ctx: VisualCtx, demo: Priority1DemoInput): void {
  const failed = ctx.observations.filter((o) => !o.ok).map((o) => o.label);
  console.log("\n════════ 人机协作 — 请人工继续 ════════");
  console.log("[human-assist] 脚本已自动完成 Step 1 与 Step 2 可执行部分。");
  if (failed.length) {
    console.log(`[human-assist] 以下字段自动填写未确认，请人工核对：${failed.join("、")}`);
  }
  console.log("[human-assist] 请在浏览器中完成：");
  if (demo.ftlAdditionalServices.includes("full_ftl")) {
    console.log("  · 勾选 Additional Services → Full FTL（若未选中）");
  }
  console.log(`  · What are you shipping? → ${demo.commodity}`);
  console.log(`  · Total Weight → ${demo.weightLb} lbs`);
  if (demo.ftlTrailer !== "ftl_expedited") {
    console.log(`  · Pallets → ${demo.palletCount}`);
  }
  console.log("  · 点击 GET INSTANT QUOTES 提交");
  console.log("[human-assist] 脚本正在记录您的操作，出现报价后自动保存录制。\n");
}

function saveHumanAssistRecording(
  outDir: string,
  caseId: string,
  events: HumanAssistEvent[],
  opts: {
    recordingBase?: string;
    manifest?: Omit<HumanAccessorialRecording, "events" | "playwrightScript">;
  } = {},
): { jsonPath: string; jsPath: string } {
  const ts = new Date().toISOString().replace(/[:.]/g, "").slice(0, 15);
  const recordingsDir = join(process.cwd(), ".rpa", "recordings");
  mkdirSync(recordingsDir, { recursive: true });
  const base = opts.recordingBase ?? `priority1-human-${caseId}-${ts}`;
  const jsonPath = join(recordingsDir, `${base}.json`);
  const jsPath = join(recordingsDir, `${base}.js`);
  const playwrightScript = formatHumanEventsAsPlaywright(events);
  const payload: HumanAccessorialRecording | { events: HumanAssistEvent[] } = opts.manifest
    ? { ...opts.manifest, events, playwrightScript, recordedAt: opts.manifest.recordedAt }
    : { events };
  writeFileSync(jsonPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
  writeFileSync(jsPath, `${playwrightScript}\n`, "utf8");
  writeFileSync(join(outDir, "human-recording.json"), `${JSON.stringify(payload, null, 2)}\n`, "utf8");
  writeFileSync(join(outDir, "human-recording.js"), `${playwrightScript}\n`, "utf8");
  return { jsonPath, jsPath };
}

async function runHumanAssistPhase(
  ctx: VisualCtx,
  opts: RunPriority1VisualChainOptions,
  browser: Awaited<ReturnType<typeof chromium.launch>>,
  context: Awaited<ReturnType<typeof browser.newContext>>,
  page: Page,
  startedAt: number,
  runId: string,
  caseId: string,
  outDir: string,
  demo: Priority1DemoInput,
): Promise<Priority1VisualChainResult> {
  const skipAccessorials = opts.humanAssistHandoff === "ltlAccessorials";
  const skipFtlAdditional = opts.humanAssistHandoff === "ftlAdditionalServices";
  console.log(
    skipAccessorials
      ? "[human-assist] ▶ Step 2 自动填写（跳过 LTL 附加服务，交人工勾选）"
      : skipFtlAdditional
        ? "[human-assist] ▶ Step 2 自动填写（跳过 FTL Additional Services，交人工）"
        : "[human-assist] ▶ Step 2 软自动填写（失败不中断）",
  );
  await fillStep2(ctx, true, { skipAccessorials, skipFtlAdditionalServices: skipFtlAdditional });
  await page.screenshot({
    path: join(outDir, "step2-handoff-full.png"),
    fullPage: true,
  });

  await installHumanAssistRecorder(page);
  if (skipAccessorials) {
    printP08AccessorialHandoff();
  } else if (skipFtlAdditional) {
    printFtlAdditionalHandoff(demo);
  } else {
    printHumanAssistHandoff(ctx, demo);
  }

  const maxWait = opts.humanAssistMaxWaitMs ?? 30 * 60 * 1000;
  const pollMs = 2_000;
  const started = Date.now();
  let lastEventCount = 0;
  let accumulatedEvents: HumanAssistEvent[] = [];
  let quoteDetected = false;
  let shuttingDown = false;

  const canonical = PRIORITY1_CANONICAL_PATHS.find((p) => p.id === caseId);

  const persistRecording = (events: HumanAssistEvent[]) => {
    const ts = new Date().toISOString().replace(/[:.]/g, "").slice(0, 15);
    const recordingBase =
      opts.humanAssistHandoff === "ltlAccessorials"
        ? `priority1-P08-accessorials-human-${ts}`
        : skipFtlAdditional
          ? `priority1-${caseId}-human-${ts}`
          : undefined;
    return saveHumanAssistRecording(outDir, caseId, events, {
      recordingBase,
      manifest: skipAccessorials
        ? {
            pathId: "P08-ltl-all-accessorials",
            title: "LTL 全附加服务 — 人工勾选 11 项",
            recordedAt: new Date().toISOString(),
            expectedAccessorials: P08_LTL_ACCESSORIAL_LABELS,
          }
        : skipFtlAdditional && canonical
          ? {
              pathId: canonical.id,
              title: `${canonical.title} — 人工补录 Step2`,
              recordedAt: new Date().toISOString(),
              expectedAccessorials: demo.ftlAdditionalServices.map(
                (s) => FTL_ADDITIONAL[s].label,
              ),
            }
          : undefined,
    });
  };

  const finishAssist = async (reason: string, withQuotes: boolean) => {
    if (shuttingDown) return null;
    shuttingDown = true;
    const drained = await drainHumanAssistEvents(page).catch(() => [] as HumanAssistEvent[]);
    const events =
      drained.length >= accumulatedEvents.length ? drained : accumulatedEvents;
    let jsonPath = "";
    let jsPath = "";
    if (events.length > 0 || quoteDetected || withQuotes) {
      ({ jsonPath, jsPath } = persistRecording(events));
      console.log(`\n[human-assist] ${reason}`);
      console.log(`[human-assist] 记录事件 ${events.length} 条`);
      console.log(`[human-assist] 人工录制: ${jsonPath}`);
      console.log(`[human-assist] Playwright 脚本: ${jsPath}`);
    }
    return { events, jsonPath, jsPath };
  };

  process.once("SIGINT", () => {
    void (async () => {
      await finishAssist("用户 Ctrl+C — 已保存录制", quoteDetected);
      await context.close().catch(() => undefined);
      await browser.close().catch(() => undefined);
      process.exit(0);
    })();
  });

  while (Date.now() - started < maxWait && !shuttingDown) {
    if (page.isClosed()) {
      await finishAssist("浏览器已关闭 — 已保存已记录操作", quoteDetected);
      throw new Error("浏览器已关闭");
    }

    const drained = await drainHumanAssistEvents(page).catch(() => [] as HumanAssistEvent[]);
    if (drained.length > accumulatedEvents.length) {
      accumulatedEvents = drained;
    }
    if (accumulatedEvents.length > lastEventCount) {
      const latest = accumulatedEvents[accumulatedEvents.length - 1]!;
      console.log(
        `[human-assist] 记录 #${accumulatedEvents.length}: ${latest.kind} ${latest.label || latest.selector || latest.text.slice(0, 30)}`,
      );
      lastEventCount = accumulatedEvents.length;
    }

    const domText = await page.locator("body").innerText().catch(() => "");
    const manualFollowup = isPriority1ManualFollowupPage(domText);
    const visible = extractPriority1VisibleQuotes(ctx.networkBodies, domText);
    if (manualFollowup || visible.length > 0) {
      quoteDetected = true;
      const quoteWait = manualFollowup
        ? resolvePriority1SubmitOutcome(ctx.networkBodies, domText)
        : await waitForQuotes(ctx);
      if (manualFollowup) {
        await extractAndSaveQuotes(ctx, quoteWait.outcome, quoteWait.message);
      }
      await finishAssist(
        manualFollowup
          ? `✓ ${quoteWait.message ?? PRIORITY1_MANUAL_FOLLOWUP_MESSAGE}`
          : "✓ 已捕获报价",
        true,
      );

      const allObservedOk = ctx.observations.every((o) => o.ok);
      const report: Priority1VisualChainResult = {
        runId,
        caseId,
        outDir,
        demo,
        finishedAt: new Date().toISOString(),
        durationMs: Date.now() - startedAt,
        finalUrl: page.url(),
        observations: ctx.observations,
        quotes: quoteWait.quotes,
        quoteCount: quoteWait.quotes.length,
        quoteOutcome: quoteWait.outcome,
        message: quoteWait.message,
        allObservedOk,
        ok:
          quoteWait.outcome === "instant_quote"
            ? quoteWait.quotes.length > 0
            : quoteWait.outcome !== "no_result" &&
              (quoteWait.outcome === "manual_followup" || allObservedOk),
      };
      writeFileSync(join(outDir, "observations.json"), `${JSON.stringify(report, null, 2)}\n`, "utf8");
      if (quoteWait.outcome === "manual_followup") {
        console.log(formatPriority1QuoteOutcomeMessage(quoteWait.outcome, quoteWait.message));
      } else {
        console.log(formatPriority1QuotesTable(quoteWait.quotes));
      }

      const keepOpen = opts.keepBrowserOpen ?? shouldKeepBrowserOpen();
      if (keepOpen) {
        console.log("\n[human-assist] 浏览器保持打开核对报价。Ctrl+C 结束。");
        await new Promise<void>((resolve) => {
          process.once("SIGINT", () => resolve());
          process.once("SIGTERM", () => resolve());
        });
      }
      await context.close();
      await browser.close();
      return report;
    }

    await page.waitForTimeout(pollMs);
  }

  await finishAssist("人机协作超时 — 已保存已记录操作", quoteDetected);
  throw new Error(`人机协作超时（${maxWait / 60000} 分钟）未出现报价`);
}

export async function runPriority1HumanAssistChain(
  opts: Omit<RunPriority1VisualChainOptions, "humanAssistMode"> = {},
): Promise<Priority1VisualChainResult> {
  return runPriority1VisualChain({
    ...opts,
    humanAssistMode: true,
    keepBrowserOpen: opts.keepBrowserOpen ?? true,
  });
}

/** FTL canonical — 自动填至拖车/条件字段，人工录 Additional Services + 货描/重量/提交 */
export async function runPriority1FtlPathRecording(
  opts: Omit<RunPriority1VisualChainOptions, "humanAssistMode" | "humanAssistHandoff"> = {},
): Promise<Priority1VisualChainResult> {
  return runPriority1VisualChain({
    ...opts,
    humanAssistMode: true,
    humanAssistHandoff: "ftlAdditionalServices",
    keepBrowserOpen: opts.keepBrowserOpen ?? true,
  });
}

/** P08 — 自动填至附加服务前，人工勾选 11 项并录制至报价 */
export async function runPriority1P08AccessorialRecording(
  opts: Omit<RunPriority1VisualChainOptions, "humanAssistMode" | "humanAssistHandoff"> = {},
): Promise<Priority1VisualChainResult> {
  const canonical = PRIORITY1_CANONICAL_PATHS.find((p) => p.id === "P08-ltl-all-accessorials");
  return runPriority1VisualChain({
    ...opts,
    demo: canonical?.input,
    caseId: "P08-ltl-all-accessorials",
    humanAssistMode: true,
    humanAssistHandoff: "ltlAccessorials",
    keepBrowserOpen: opts.keepBrowserOpen ?? true,
  });
}

async function submitStep2(ctx: VisualCtx): Promise<Priority1QuoteWaitResult> {
  await ctx.page
    .locator("#container-mh")
    .click({ position: { x: 10, y: 10 } })
    .catch(() => undefined);
  await ctx.page.waitForTimeout(400);

  const btn = ctx.page
    .locator("#container-mh")
    .getByRole("button", { name: /GET INSTANT QUOTES/i })
    .first();
  await highlight(btn);
  await snapLocator(ctx, btn, "step2-submit-btn");
  await btn.scrollIntoViewIfNeeded().catch(() => undefined);
  await btn.click(FEATHERY_CLICK);
  await ctx.page.waitForTimeout(1_500);
  await btn.click(FEATHERY_CLICK).catch(() => undefined);

  const result = await waitForQuotes(ctx);
  if (result.outcome === "manual_followup") {
    console.log(`[观测] ✓ ${result.message ?? PRIORITY1_MANUAL_FOLLOWUP_MESSAGE}`);
    return result;
  }
  if (!result.quotes.length) {
    await ctx.page.screenshot({
      path: join(ctx.outDir, "quote-result-no-price.png"),
      fullPage: true,
    });
    throw new Error("提交 Step 2 后未能提取报价（网络与页面均无金额，且非人工跟进页）");
  }
  return result;
}

async function waitFeatheryStep1(page: Page): Promise<void> {
  await page.locator("#container-mh").waitFor({ state: "visible", timeout: 60_000 });
  await page
    .locator("#container-mh")
    .getByText(/Shipment Type/i)
    .first()
    .waitFor({ state: "visible" });
  await page.waitForTimeout(800);
}

export type Priority1VisualChainResult = {
  runId: string;
  caseId: string;
  outDir: string;
  demo: Priority1DemoInput;
  finishedAt: string;
  durationMs: number;
  finalUrl: string;
  observations: Observation[];
  quotes: Priority1QuoteLine[];
  quoteCount: number;
  quoteOutcome?: Priority1QuoteOutcome;
  message?: string;
  allObservedOk: boolean;
  ok: boolean;
  error?: string;
};

export type RunPriority1VisualChainOptions = {
  demo?: Priority1DemoInput;
  caseId?: string;
  outDir?: string;
  runId?: string;
  keepBrowserOpen?: boolean;
  /** 仅核对 Step 2 填写，不提交报价（inspect 模式） */
  stopAfterStep2?: boolean;
  /** 自动填至卡住后交人工，记录人工操作直至报价 */
  humanAssistMode?: boolean;
  /** ltlAccessorials = Step2 跳过 LTL 附加；ftlAdditionalServices = 跳过 FTL Additional Services */
  humanAssistHandoff?: "ltlAccessorials" | "ftlAdditionalServices";
  humanAssistMaxWaitMs?: number;
  /** Worker 生产询价：跳过截图/停留，缩短观测与轮询 */
  workerFast?: boolean;
};

export async function runPriority1VisualChain(
  opts: RunPriority1VisualChainOptions = {},
): Promise<Priority1VisualChainResult> {
  const demo = opts.demo ?? parsePriority1DemoFromEnv(process.env.PRIORITY1_DEMO_JSON);
  const caseId = opts.caseId ?? process.env.PRIORITY1_CASE_ID?.trim() ?? "single";
  const workerFast =
    typeof opts.workerFast === "boolean"
      ? opts.workerFast
      : caseId.startsWith("worker-") ||
        process.env.RPA_PRIORITY1_WORKER_FAST === "true";
  const runId = opts.runId ?? randomUUID().slice(0, 8);
  const outDir =
    opts.outDir ??
    (process.env.PRIORITY1_OUT_DIR?.trim()
      ? process.env.PRIORITY1_OUT_DIR.trim()
      : join(
          process.cwd(),
          ".rpa",
          "visual-priority1",
          process.env.PRIORITY1_BATCH_DIR?.trim()
            ? join(process.env.PRIORITY1_BATCH_DIR.trim(), caseId)
            : runId,
        ));
  mkdirSync(outDir, { recursive: true });

  console.log(`\n[visual-p1] Priority1 可视回放 ${runId} / ${caseId}`);
  console.log("[visual-p1] 推进规则：仅以输入框/下拉/复选框可见状态 + 元素截图为据");
  const forceHeaded = Boolean(opts.humanAssistMode);
  console.log(`[visual-p1] 类型=${demo.shipmentType} headed=${forceHeaded || !envHeadless()} slowMo=${envSlowMo() ?? 0} dwell=${envDwellMs()}ms`);
  console.log(`[visual-p1] 观测截图目录: ${outDir}\n`);

  const browser = await launchRpaBrowser({
    headless: forceHeaded ? false : envHeadless(),
    ...(envSlowMo() !== undefined ? { slowMo: envSlowMo() } : {}),
  });

  const context = await createRpaBrowserContext(browser);
  const page = await context.newPage();
  page.setDefaultTimeout(90_000);

  const ctx: VisualCtx = {
    page,
    outDir,
    demo,
    observations: [],
    snapSeq: 0,
    networkBodies: [],
    quotes: [],
    workerFast,
  };
  attachPriority1QuoteNetworkCapture(page, ctx.networkBodies);
  const startedAt = Date.now();

  try {
    console.log("[visual-p1] ▶ 打开询价页");
    await gotoWithResilience(page, ENTRY_URL);
    await waitFeatheryStep1(page);
    await page.screenshot({
      path: join(outDir, "00-step1-empty-full.png"),
      fullPage: true,
    });

    console.log("[visual-p1] ▶ Step 1 逐字段填写 + 观测");
    await fillStep1(ctx);
    await page.screenshot({
      path: join(outDir, "step1-all-fields-full.png"),
      fullPage: true,
    });

    console.log("[visual-p1] ▶ 提交 Step 1");
    await submitStep1(ctx);

    if (opts.humanAssistMode) {
      return runHumanAssistPhase(
        ctx,
        opts,
        browser,
        context,
        page,
        startedAt,
        runId,
        caseId,
        outDir,
        demo,
      );
    }

    console.log("[visual-p1] ▶ Step 2 逐字段填写 + 观测");
    await fillStep2(ctx);
    await page.screenshot({
      path: join(outDir, "step2-all-fields-full.png"),
      fullPage: true,
    });

    if (opts.stopAfterStep2) {
      console.log("[visual-p1] ▶ Step 2 填写完成（inspect 模式，跳过报价提交）");
      const report: Priority1VisualChainResult = {
        runId,
        caseId,
        outDir,
        demo,
        finishedAt: new Date().toISOString(),
        durationMs: Date.now() - startedAt,
        finalUrl: page.url(),
        observations: ctx.observations,
        quotes: [],
        quoteCount: 0,
        allObservedOk: ctx.observations.every((o) => o.ok),
        ok: ctx.observations.every((o) => o.ok),
      };
      const reportPath = join(outDir, "observations.json");
      writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
      console.log("\n════════ 观测汇总 ════════");
      for (const o of ctx.observations) {
        console.log(
          `  ${o.ok ? "✓" : "✗"} ${o.label}: "${o.observed}" ← ${o.screenshot}`,
        );
      }
      console.log(`\n观测报告: ${reportPath}`);
      const keepOpen =
        opts.keepBrowserOpen ?? shouldKeepBrowserOpen();
      if (keepOpen) {
        console.log("\n[visual-p1] 浏览器保持打开，请肉眼核对 Step 2。Ctrl+C 结束。");
        await new Promise<void>((resolve) => {
          process.once("SIGINT", () => resolve());
          process.once("SIGTERM", () => resolve());
        });
      } else if (envDwellMs() > 0) {
        await page.waitForTimeout(envDwellMs());
      }
      await context.close();
      await browser.close();
      return report;
    }

    console.log("[visual-p1] ▶ 提交 Step 2 → 报价");
    const quoteWait = await submitStep2(ctx);
    const allObservedOk = ctx.observations.every((o) => o.ok);

    const report: Priority1VisualChainResult = {
      runId,
      caseId,
      outDir,
      demo,
      finishedAt: new Date().toISOString(),
      durationMs: Date.now() - startedAt,
      finalUrl: page.url(),
      observations: ctx.observations,
      quotes: quoteWait.quotes,
      quoteCount: quoteWait.quotes.length,
      quoteOutcome: quoteWait.outcome,
      message: quoteWait.message,
      allObservedOk,
      ok:
        quoteWait.outcome === "instant_quote"
          ? quoteWait.quotes.length > 0
          : quoteWait.outcome !== "no_result" &&
            (quoteWait.outcome === "manual_followup" || allObservedOk),
    };

    const reportPath = join(outDir, "observations.json");
    writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
    await context.storageState({
      path: join(process.cwd(), ".rpa", "priority1-storage.json"),
    });

    console.log("\n════════ 观测汇总 ════════");
    for (const o of ctx.observations) {
      console.log(
        `  ${o.ok ? "✓" : "✗"} ${o.label}: "${o.observed}" ← ${o.screenshot}`,
      );
    }
    console.log(`\n观测报告: ${reportPath}`);
    console.log(`截图目录: ${outDir}`);

    const keepOpen =
      opts.keepBrowserOpen ?? shouldKeepBrowserOpen();
    if (keepOpen) {
      console.log("\n[visual-p1] 浏览器保持打开，请肉眼核对各输入框。Ctrl+C 结束。");
      await new Promise<void>((resolve) => {
        process.once("SIGINT", () => resolve());
        process.once("SIGTERM", () => resolve());
      });
    } else if (envDwellMs() > 0) {
      await page.waitForTimeout(envDwellMs());
    }

    await context.close();
    await browser.close();
    return report;
  } catch (error) {
    const raw = error instanceof Error ? error.message : String(error);
    const errMsg = isTransientNavigationError(error)
      ? formatExternalSiteNavigationError("Priority1 官网", error)
      : raw;
    writeFileSync(
      join(outDir, "observations-partial.json"),
      `${JSON.stringify({ runId, caseId, error: errMsg, observations: ctx.observations }, null, 2)}\n`,
      "utf8",
    );
    console.error(`[visual-p1] 中断: ${errMsg}`);

    const failedObs = ctx.observations.filter((o) => !o.ok);
    if (failedObs.length) {
      console.log("\n════════ 中断点（最后失败观测）════════");
      for (const o of failedObs) {
        console.log(`  ✗ [${o.step}] ${o.label}`);
        console.log(`     期望: ${o.expected} | 实际: ${o.observed}`);
        console.log(`     截图: ${o.screenshot}`);
      }
    }

    const keepOpen =
      opts.keepBrowserOpen ?? shouldKeepBrowserOpen();
    if (keepOpen) {
      console.log(
        "\n[visual-p1] 浏览器保持打开 — 请肉眼核对中断字段。Ctrl+C 关闭并继续。",
      );
      await new Promise<void>((resolve) => {
        process.once("SIGINT", () => resolve());
        process.once("SIGTERM", () => resolve());
      });
    } else if (!envHeadless() && envDwellMs() > 0) {
      await page.waitForTimeout(envDwellMs());
    }

    await context.close().catch(() => undefined);
    await browser.close().catch(() => undefined);
    return {
      runId,
      caseId,
      outDir,
      demo,
      finishedAt: new Date().toISOString(),
      durationMs: Date.now() - startedAt,
      finalUrl: page.url(),
      observations: ctx.observations,
      quotes: ctx.quotes,
      quoteCount: ctx.quotes.length,
      quoteOutcome: "no_result",
      allObservedOk: ctx.observations.every((o) => o.ok),
      ok: false,
      error: errMsg,
    };
  }
}
