/**
 * MotherShip 鐧诲綍鎬?dashboard銆孋reate a new shipment銆嶁啋 rate-card 鏌ヤ环
 * 鏈夎处瀵嗘椂绂佹璧板尶鍚?Axel Direct锛堜环闈㈡槸 TForce/Daylight 妗ｄ綅锛岄潪 ABF/XPO Direct锛? */
import type { Page } from "playwright";
import fs from "node:fs";
import path from "node:path";
import { launchRpaBrowser } from "@/lib/rpa/browser-launch";
import { resolveStorageStatePath } from "@/lib/axel/session";
import {
  getEffectiveMothershipLogin,
  hasEffectiveMothershipLogin,
} from "@/lib/rpa/mothership-login-context";
import type { QuoteItem, QuoteRequest } from "@/modules/providers/quote-provider";
import { PROVIDER_LOGIN_FAILED_USER_MESSAGE } from "@/modules/rpa/provider-login-message";
import { RpaError } from "@/modules/rpa/errors";
import {
  isMothershipWeightEachAllowed,
  MOTHERSHIP_MAX_WEIGHT_EACH_LB,
  normalizeMothershipReadyDateIso,
  snapMothershipReadyDateToWeekday,
} from "@/lib/mothership/logged-in-constraints";
import {
  extractMothershipAccessorialBlockMessages,
  formatMothershipPortalQuoteMessage,
  parseMothershipPortalQuoteMessageFromBody,
} from "@/lib/mothership/portal-quote-messages";
import { normalizeQuoteItems } from "@/workers/rpa/quote-capture/quote-schema-validator";
import {
  createContext,
  withRpaSessionLock,
} from "@/workers/rpa/session-manager";

/** 登录后一级查价稳定入口（录制 mothership-logged-in-20260715-140935.js） */
export const MOTHERSHIP_CREATE_SHIPMENT_URL =
  "https://dashboard.mothership.com/ship";
/** Continue 后等到价卡：含 Inbox 关闭 / Save&update / 慢车道 */
const RATE_WAIT_MS = 120_000;
/** Continue 后先等一级自然出价，避免加载期误判缺二级必填 */
const RATE_GRACE_AFTER_CONTINUE_MS = 25_000;
const FIELD_WAIT_MS = 25_000;
/** 濉〃寰仠锛氬敖閲忕煭锛岄潬 waitFor 鑰岄潪鐩茬瓑 */
const PAUSE_XS = 80;
const PAUSE_SM = 150;
const PAUSE_MD = 280;
const SUGGEST_WAIT_MS = 1_200;

export function parseLoggedInRateCardText(text: string): QuoteItem | null {
  const compact = text.replace(/\s+/g, " ").trim();
  if (!compact) return null;

  const priceMatch = compact.match(/\$\s*([0-9,]+\.\d{2})/);
  if (!priceMatch) return null;
  const total = Number(priceMatch[1]!.replace(/,/g, ""));
  if (!(Number.isFinite(total) && total > 0)) return null;

  // 为何：官网文案多样；解析失败时不得写 "—"（validateQuoteSchema 会报「时效为空」）
  const daysMatch =
    compact.match(/(\d+)\s*-\s*(\d+)\s*business\s*days?/i) ||
    compact.match(/(\d+)\s*business\s*days?/i) ||
    compact.match(/Est\.?\s*(\d+)/i) ||
    compact.match(/Estimated\s+(\d+)/i) ||
    compact.match(/(\d+)\s*个?工作日/) ||
    compact.match(/(\d+)\s*-\s*(\d+)\s*days?/i) ||
    compact.match(/(\d+)\s*days?(?!\s*ago)/i);
  const days = daysMatch
    ? daysMatch[2]
      ? `${daysMatch[1]}-${daysMatch[2]}`
      : daysMatch[1]!
    : "待确认";

  let carrier = "MotherShip";
  const direct = compact.match(
    /(?:Selected|Checkmark|已选)?\s*([A-Za-z0-9][A-Za-z0-9 &.+/-]{0,30}?)\s+Direct\b/i,
  );
  if (direct?.[1]) {
    carrier = `${direct[1].trim()} Direct`;
  } else {
    const interline = compact.match(
      /([A-Za-z0-9][A-Za-z0-9 &.+/-]{0,40}?)\s+Interline\b/i,
    );
    if (interline?.[1]) {
      carrier = `${interline[1].trim()} Interline`;
    } else {
      const selected = compact.match(
        /(?:Selected|已选)\s+([A-Za-z0-9][A-Za-z0-9 &.+/-]{1,40})/i,
      );
      if (selected?.[1]) carrier = selected[1].trim();
    }
  }

  const guaranteed = /guaranteed|保证送达/i.test(compact);
  return {
    serviceLevel: guaranteed ? "guaranteed" : "standard",
    rateOption: "bestValue",
    carrier,
    transitDays: String(days),
    transitDescription:
      days === "待确认" ? "时效待定" : `${days} business days`,
    rawFreight: total,
    surcharges: 0,
    rawTotal: total,
  };
}

export function buildLoggedInAddressSearchQuery(
  addr: QuoteRequest["pickup"],
): string {
  const stripUnitNoise = (s: string) =>
    s
      .replace(
        /\b(?:floor|fl|suite|ste|unit|apt|rm|room)\s*#?\s*[\w-]+\b/gi,
        "",
      )
      .replace(/\s{2,}/g, " ")
      .replace(/\s+,/g, ",")
      .trim();

  const street = stripUnitNoise(addr.street?.trim() || "");
  const structured = [street, addr.city, addr.state, addr.zip]
    .map((p) => p?.trim())
    .filter(Boolean)
    .join(", ");
  if (street && addr.city?.trim() && addr.state?.trim()) {
    return structured;
  }
  const fallback =
    addr.mothershipDisplayLabel?.trim() ||
    addr.formattedAddress?.trim() ||
    structured;
  return stripUnitNoise(fallback);
}

function addressQuery(addr: QuoteRequest["pickup"]): string {
  return buildLoggedInAddressSearchQuery(addr);
}

/** dashboard 鍏ㄥ眬銆孲earch any shipment銆嶉伄缃╋紙璇Е Search / 蹇嵎閿細寮瑰嚭锛?*/
export async function isGlobalShipmentSearchModalOpen(
  page: Page,
): Promise<boolean> {
  const markers = [
    page.getByRole("heading", { name: /Search any shipment/i }),
    page.getByPlaceholder(/Search any shipment/i),
    page.getByText(/Search by shipment#.*business name/i),
  ];
  for (const loc of markers) {
    if ((await loc.count()) > 0 && (await loc.first().isVisible())) {
      return true;
    }
  }
  return false;
}

export async function dismissGlobalShipmentSearchModal(
  page: Page,
): Promise<boolean> {
  if (!(await isGlobalShipmentSearchModalOpen(page))) {
    return false;
  }
  console.log("[rpa] logged-in: 鍏抽棴 Search any shipment 鍏ㄥ眬寮圭獥");

  await page.keyboard.press("Escape");
  await page.waitForTimeout(PAUSE_SM);
  if (!(await isGlobalShipmentSearchModalOpen(page))) {
    return true;
  }

  const closeCandidates = [
    page.getByRole("button", { name: /^close$/i }),
    page.getByRole("button", { name: /close dialog/i }),
    page.locator('button[aria-label*="close" i]'),
    page.getByRole("button").filter({ hasText: /^×$|^X$/ }),
  ];
  for (const btn of closeCandidates) {
    if ((await btn.count()) > 0) {
      try {
        await btn.first().click({ timeout: 2_000 });
        await page.waitForTimeout(PAUSE_SM);
        if (!(await isGlobalShipmentSearchModalOpen(page))) {
          return true;
        }
      } catch {
        /* try next */
      }
    }
  }

  await page.keyboard.press("Escape");
  await page.waitForTimeout(PAUSE_SM);
  return !(await isGlobalShipmentSearchModalOpen(page));
}

async function isLoginFormVisible(page: Page): Promise<boolean> {
  const email = page.getByTestId("auth-email-input");
  if ((await email.count()) === 0) return false;
  return email.first().isVisible().catch(() => false);
}

/** goto /ship 后等 SPA 落定：表单 or 登录页（避免重定向前误跳过登录） */
async function waitForShipOrLogin(
  page: Page,
  timeoutMs = 20_000,
): Promise<"form" | "login" | "unknown"> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (await isCreateShipmentFormVisible(page)) return "form";
    const url = page.url().toLowerCase();
    if (
      url.includes("/login") ||
      url.includes("/sign-in") ||
      (await isLoginFormVisible(page))
    ) {
      return "login";
    }
    await page.waitForTimeout(200);
  }
  if (await isCreateShipmentFormVisible(page)) return "form";
  const url = page.url().toLowerCase();
  if (
    url.includes("/login") ||
    url.includes("/sign-in") ||
    (await isLoginFormVisible(page))
  ) {
    return "login";
  }
  return "unknown";
}

async function loginDashboardIfNeeded(page: Page): Promise<void> {
  const url = page.url().toLowerCase();
  const onLoginUrl = url.includes("/login") || url.includes("/sign-in");
  // 为何：会话过期时 URL 可能仍停在 /ship，仅靠 path 会跳过登录
  if (!onLoginUrl && !(await isLoginFormVisible(page))) {
    return;
  }
  const creds = getEffectiveMothershipLogin();
  if (!creds) {
    throw new RpaError(
      "STRUCT_CHANGE",
      "dashboard 登录页但无 MotherShip 账密（请管理端配置客户账密，或 .env 填 MOTHERSHIP_EMAIL/PASSWORD，或上传 mothership-logged-in-storage.json）",
      { retryable: false },
    );
  }

  console.log(
    "[rpa] logged-in: dashboard login email=" +
      creds.email.slice(0, 2) +
      "***",
  );
  const email = page.getByTestId("auth-email-input");
  const password = page.getByTestId("auth-password-input");
  const submit = page.getByTestId("auth-log-in-button");
  try {
    if ((await email.count()) > 0) {
      await email.waitFor({ state: "visible", timeout: 10_000 });
      await email.fill(creds.email);
      await password.fill(creds.password);
      await submit.click();
    } else {
      await page.fill(
        process.env.RPA_SELECTOR_LOGIN_EMAIL ??
          'input[type="email"], input[name="email"]',
        creds.email,
      );
      await page.fill(
        process.env.RPA_SELECTOR_LOGIN_PASSWORD ??
          'input[type="password"], input[name="password"]',
        creds.password,
      );
      await page.click(
        process.env.RPA_SELECTOR_LOGIN_SUBMIT ??
          'button[type="submit"], button:has-text("Sign in"), button:has-text("Log in")',
      );
    }
    await page.waitForURL((u) => !/login|sign-in/i.test(u.href), {
      timeout: 30_000,
    });
  } catch (err) {
    const brief = err instanceof Error ? err.message : String(err);
    console.warn(
      "[rpa] logged-in: dashboard login failed, still on login page. detail=" +
        brief.slice(0, 200),
    );
    throw new RpaError("PROVIDER_LOGIN_FAILED", PROVIDER_LOGIN_FAILED_USER_MESSAGE, {
      retryable: false,
    });
  }
  if (
    /\/login|sign-in/i.test(page.url()) ||
    (await isLoginFormVisible(page))
  ) {
    throw new RpaError("PROVIDER_LOGIN_FAILED", PROVIDER_LOGIN_FAILED_USER_MESSAGE, {
      retryable: false,
    });
  }
}

async function isCreateShipmentFormVisible(page: Page): Promise<boolean> {
  const pickup = page.getByTestId("quote-create-pickup-input-search");
  if ((await pickup.count()) === 0) return false;
  return pickup.first().isVisible().catch(() => false);
}

/** 等 SPA 渲染出提货搜索框；失败返回 false（不抛） */
async function waitForCreateShipmentForm(
  page: Page,
  timeoutMs = FIELD_WAIT_MS,
): Promise<boolean> {
  try {
    await page
      .getByTestId("quote-create-pickup-input-search")
      .waitFor({ state: "visible", timeout: timeoutMs });
    return true;
  } catch {
    return false;
  }
}

async function gotoCreateShipmentUrl(page: Page): Promise<void> {
  console.log(
    "[rpa] logged-in: goto create shipment url=" + MOTHERSHIP_CREATE_SHIPMENT_URL,
  );
  await page.goto(MOTHERSHIP_CREATE_SHIPMENT_URL, {
    waitUntil: "domcontentloaded",
    timeout: 60_000,
  });
  await dismissGlobalShipmentSearchModal(page);
}

async function openCreateShipment(page: Page): Promise<void> {
  await dismissGlobalShipmentSearchModal(page);
  if (await isCreateShipmentFormVisible(page)) {
    return;
  }

  // 为何：直达 /ship；须等落定到表单或登录页，禁止重定向前跳过登录
  await gotoCreateShipmentUrl(page);
  let state = await waitForShipOrLogin(page);
  console.log("[rpa] logged-in: after /ship state=" + state + " url=" + page.url());
  if (state === "login") {
    await loginDashboardIfNeeded(page);
    await gotoCreateShipmentUrl(page);
    state = await waitForShipOrLogin(page);
    console.log(
      "[rpa] logged-in: after login state=" + state + " url=" + page.url(),
    );
  } else {
    await loginDashboardIfNeeded(page);
  }
  await dismissGlobalShipmentSearchModal(page);
  if (state === "form" || (await waitForCreateShipmentForm(page, FIELD_WAIT_MS))) {
    return;
  }

  // 登录后可能落首页，再强制 /ship
  console.log("[rpa] logged-in: form missing after login, retry /ship");
  await gotoCreateShipmentUrl(page);
  state = await waitForShipOrLogin(page);
  if (state === "login") {
    await loginDashboardIfNeeded(page);
    await gotoCreateShipmentUrl(page);
    state = await waitForShipOrLogin(page);
  }
  await dismissGlobalShipmentSearchModal(page);
  if (state === "form" || (await waitForCreateShipmentForm(page, FIELD_WAIT_MS))) {
    return;
  }

  // 兜底：旧路径 Ship → Create a new shipment（短超时）
  const shipLink = page.getByRole("link", { name: /^Ship$/i });
  if ((await shipLink.count()) > 0) {
    console.log("[rpa] logged-in: fallback click Ship menu");
    await shipLink.first().click();
    await page.waitForTimeout(PAUSE_MD);
    await dismissGlobalShipmentSearchModal(page);
    const create = page.getByText(
      /Create a new shipment|Create shipment|创建新货件|新建货件/i,
    );
    const menuOk = await create
      .first()
      .waitFor({ state: "visible", timeout: 8_000 })
      .then(() => true)
      .catch(() => false);
    if (menuOk) {
      await create.first().click();
      await dismissGlobalShipmentSearchModal(page);
      if (await waitForCreateShipmentForm(page, FIELD_WAIT_MS)) {
        return;
      }
    }
  }

  const body = (await page.locator("body").innerText().catch(() => "")).slice(
    0,
    400,
  );
  const stillLogin =
    /\/login|sign-in/i.test(page.url()) ||
    /Sign in to Mothership|auth-email-input|Forgot password/i.test(body);
  if (stillLogin) {
    throw new RpaError(
      "PROVIDER_LOGIN_FAILED",
      PROVIDER_LOGIN_FAILED_USER_MESSAGE,
      { retryable: false },
    );
  }
  throw new RpaError(
    "STRUCT_CHANGE",
    "无法打开登录态 Create shipment 表单（无 quote-create-pickup-input-search）。url=" +
      page.url() +
      " body=" +
      body.replace(/\s+/g, " ").trim(),
    { retryable: true },
  );
}

/**
 * 校验地址输入框已确认（非占位/非错误状态）
 * 官网确认后：对应侧「Please enter the address…」占位消失，且 input 含地址 token。
 */
async function isAddressConfirmed(
  page: Page,
  side: "pickup" | "delivery",
  tokens: string[],
): Promise<boolean> {
  const testId =
    side === "pickup"
      ? "quote-create-pickup-input-search"
      : "quote-create-delivery-input-search";
  const body = await page.locator("body").innerText().catch(() => "");
  const placeholderRe =
    side === "pickup"
      ? /Please enter the address of your pick-?up location/i
      : /Please enter the address of your delivery location/i;
  if (placeholderRe.test(body)) {
    return false;
  }

  const inputVal = (
    await page
      .getByTestId(testId)
      .inputValue()
      .catch(() => "")
  ).trim();
  if (inputVal.length <= 3) {
    return false;
  }
  // 必须命中至少一个地址 token，避免仅搜索未点选却残留输入
  const lower = inputVal.toLowerCase();
  return tokens.some((t) => t.length >= 3 && lower.includes(t.toLowerCase()));
}

async function tryPickOnce(
  page: Page,
  side: "pickup" | "delivery",
  query: string,
  tokens: string[],
  t0: number,
): Promise<boolean> {
  const testId =
    side === "pickup"
      ? "quote-create-pickup-input-search"
      : "quote-create-delivery-input-search";
  const input = page.getByTestId(testId);
  await input.click({ timeout: 5_000 });
  await input.fill("");
  await input.fill(query);

  const searchBtn = page.getByRole("button", {
    name: new RegExp(
      tokens[0]
        ? `Search\\s+.*${tokens[0]!.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`
        : "^Search\\s",
      "i",
    ),
  });
  const listItem = page.getByTestId("search-list-item");

  const appeared = await new Promise<"search" | "list" | "timeout">(
    (resolve) => {
      let done = false;
      const finish = (v: "search" | "list" | "timeout") => {
        if (done) return;
        done = true;
        resolve(v);
      };
      const timer = setTimeout(() => finish("timeout"), SUGGEST_WAIT_MS);
      searchBtn
        .first()
        .waitFor({ state: "visible", timeout: SUGGEST_WAIT_MS })
        .then(() => { clearTimeout(timer); finish("search"); })
        .catch(() => undefined);
      listItem
        .first()
        .waitFor({ state: "visible", timeout: SUGGEST_WAIT_MS })
        .then(() => { clearTimeout(timer); finish("list"); })
        .catch(() => undefined);
    },
  );

  const clickOpt = { timeout: 3_000, force: true as const };

  if ((await listItem.count()) > 0 && (await listItem.first().isVisible())) {
    await listItem.first().click(clickOpt);
    await page.waitForTimeout(PAUSE_XS);
    console.log("[rpa] logged-in: suggest side=" + side + " hint=" + appeared + " via=listItem ms=" + String(Date.now() - t0));
    return true;
  }

  let clickedSearch = false;
  if ((await searchBtn.count()) > 0 && (await searchBtn.first().isVisible())) {
    await searchBtn.first().click(clickOpt);
    await page.waitForTimeout(PAUSE_XS);
    clickedSearch = true;
  }

  if ((await listItem.count()) > 0 && (await listItem.first().isVisible())) {
    await listItem.first().click(clickOpt);
    await page.waitForTimeout(PAUSE_XS);
    console.log("[rpa] logged-in: suggest side=" + side + " hint=" + appeared + " via=search+list ms=" + String(Date.now() - t0));
    return true;
  }

  // 仅点了 Search、未点选候选：不算确认（避免输入残留导致假阳性）
  if (clickedSearch) {
    console.log(
      "[rpa] logged-in: suggest side=" +
        side +
        " via=search-only（无 listItem），不算确认 ms=" +
        String(Date.now() - t0),
    );
    return false;
  }

  const buttons = page.getByRole("button");
  const n = await buttons.count();
  for (let i = 0; i < Math.min(n, 40); i += 1) {
    const btn = buttons.nth(i);
    const name = ((await btn.innerText().catch(() => "")) || "").replace(/\s+/g, " ");
    if (/^Search any shipment$/i.test(name)) continue;
    if (/^Search by shipment/i.test(name)) continue;
    if (/^Search\s/i.test(name) && !tokens.some((t) => name.includes(t))) continue;
    if (tokens.some((t) => name.toLowerCase().includes(t.toLowerCase()))) {
      await btn.click(clickOpt);
      console.log("[rpa] logged-in: suggest side=" + side + " via=tokenBtn ms=" + String(Date.now() - t0));
      return true;
    }
  }
  return false;
}

async function pickAddressSuggestion(
  page: Page,
  side: "pickup" | "delivery",
  query: string,
): Promise<void> {
  const t0 = Date.now();
  // 先关 Inbox 抽屉，否则 input/Search 点击会空等默认 30s
  await closeInboxDrawer(page);

  const tokens = query
    .split(/[,\s]+/)
    .map((t) => t.trim())
    .filter((t) => t.length >= 3)
    .slice(0, 3);

  // 最多 2 次：首次 + 失败后清空重试
  for (let attempt = 0; attempt < 2; attempt += 1) {
    if (attempt > 0) {
      console.log("[rpa] logged-in: 地址未确认，重试 side=" + side + " attempt=" + String(attempt + 1));
      await closeInboxDrawer(page);
    }
    await tryPickOnce(page, side, query, tokens, t0);
    await page.waitForTimeout(PAUSE_SM);

    // 校验地址已真正 commit
    if (await isAddressConfirmed(page, side, tokens)) {
      console.log(
        "[rpa] logged-in: address confirmed side=" + side + " ms=" + String(Date.now() - t0),
      );
      return;
    }
    console.log("[rpa] logged-in: address NOT confirmed side=" + side + " attempt=" + String(attempt + 1));
  }

  throw new RpaError(
    "ADDRESS_SUGGESTION_NOT_FOUND",
    "登录态地址未确认（官网表单仍空）（" + side + "）：" + query.slice(0, 80),
    { retryable: true },
  );
}

/** 附加服务 id → 官网可见英文（下拉文案兜底） */
const ACCESSORIAL_LABEL_RE: Record<string, RegExp> = {
  cfs: /\bCFS\b/i,
  liftgate: /liftgate/i,
  limitedAccess: /limited\s*access/i,
  inside: /inside/i,
  residential: /residential/i,
  tradeshow: /trade\s*show|tradeshow/i,
  appointment: /appointment/i,
  fbaAppointment: /fba|amazon.*appointment|appointment.*amazon/i,
};

/** 部分官网 testId 与前端 id 不完全一致时的别名 */
const ACCESSORIAL_TESTID_ALIASES: Record<string, string[]> = {
  appointment: ["appointment", "deliveryAppointment", "DeliveryAppointment"],
  fbaAppointment: ["fbaAppointment", "fba", "amazonAppointment"],
  limitedAccess: ["limitedAccess", "limited-access", "limited_access"],
};

async function listVisibleAccessorialOptionIds(page: Page): Promise<string[]> {
  return page
    .locator("[data-testid^='address-book-accessorials-option-']")
    .evaluateAll((els) =>
      els
        .map((el) => el.getAttribute("data-testid") ?? "")
        .map((id) => id.replace(/^address-book-accessorials-option-/, ""))
        .filter(Boolean),
    )
    .catch(() => [] as string[]);
}

async function openAccessorialDropdown(
  page: Page,
  side: "pickup" | "delivery",
): Promise<void> {
  if (side === "pickup") {
    const trigger = page.getByTestId("address-book-accessorials-trigger").first();
    await trigger.waitFor({ state: "visible", timeout: FIELD_WAIT_MS });
    await trigger.click();
  } else {
    const deliveryOpen = page.getByRole("button", {
      name: /Select\s+Chevron\s*down/i,
    });
    if ((await deliveryOpen.count()) > 0) {
      await deliveryOpen.last().click();
    } else {
      const triggers = page.getByTestId("address-book-accessorials-trigger");
      const n = await triggers.count();
      await triggers.nth(Math.max(0, n - 1)).click();
    }
  }
  await page.waitForTimeout(PAUSE_SM);
}

async function clickAccessorialOption(
  page: Page,
  side: "pickup" | "delivery",
  id: string,
): Promise<void> {
  const aliases = ACCESSORIAL_TESTID_ALIASES[id] ?? [id];
  for (const alias of aliases) {
    const byTestId = page.getByTestId(`address-book-accessorials-option-${alias}`);
    const count = await byTestId.count().catch(() => 0);
    if (count > 0) {
      const target = byTestId.first();
      await target.scrollIntoViewIfNeeded().catch(() => undefined);
      const visible = await target.isVisible().catch(() => false);
      if (visible) {
        await target.click({ timeout: 5_000 });
        return;
      }
      // 在 DOM 但不可见：下拉动画未完成时 force 点击
      await target.click({ force: true, timeout: 5_000 });
      return;
    }
  }

  const labelRe = ACCESSORIAL_LABEL_RE[id];
  if (labelRe) {
    const byText = page
      .locator("[data-testid^='address-book-accessorials-option-']")
      .filter({ hasText: labelRe });
    if ((await byText.count().catch(() => 0)) > 0) {
      await byText.first().click({ timeout: 5_000 });
      return;
    }
    const roleOpt = page.getByRole("option", { name: labelRe });
    if ((await roleOpt.count().catch(() => 0)) > 0) {
      await roleOpt.first().click({ timeout: 5_000 });
      return;
    }
    const roleMenuitem = page.getByRole("menuitemcheckbox", { name: labelRe });
    if ((await roleMenuitem.count().catch(() => 0)) > 0) {
      await roleMenuitem.first().click({ timeout: 5_000 });
      return;
    }
  }

  const available = await listVisibleAccessorialOptionIds(page);
  const sideZh = side === "pickup" ? "提货" : "派送";
  throw new RpaError(
    "CARRIER_NO_CAPACITY",
    `${sideZh}附加服务「${id}」在官网下拉中不可用或未展开（可见选项：${available.slice(0, 12).join(",") || "无"}）。请取消该附加服务后重试，或确认地址类型是否支持。`,
    { retryable: false },
  );
}

async function applyAccessorials(
  page: Page,
  side: "pickup" | "delivery",
  ids: string[] | undefined,
): Promise<void> {
  const wanted = [
    ...new Set((ids ?? []).map((id) => id.trim()).filter(Boolean)),
  ];
  if (wanted.length === 0) {
    console.log(`[rpa] logged-in: 跳过附加服务 side=${side}（未勾选）`);
    return;
  }

  await openAccessorialDropdown(page, side);

  // 等首个 option 出现；没有则重开一次
  const anyOption = page.locator("[data-testid^='address-book-accessorials-option-']");
  const appeared = await anyOption
    .first()
    .waitFor({ state: "visible", timeout: 8_000 })
    .then(() => true)
    .catch(() => false);
  if (!appeared) {
    console.log(`[rpa] logged-in: 附加服务下拉未展开，重试 side=${side}`);
    await page.keyboard.press("Escape").catch(() => undefined);
    await page.waitForTimeout(PAUSE_SM);
    await openAccessorialDropdown(page, side);
    await anyOption.first().waitFor({ state: "visible", timeout: 10_000 }).catch(() => {
      throw new RpaError(
        "STRUCT_CHANGE",
        `${side === "pickup" ? "提货" : "派送"}附加服务下拉未能打开，请稍后重试`,
        { retryable: true },
      );
    });
  }

  for (const id of wanted) {
    await clickAccessorialOption(page, side, id);
    await page.waitForTimeout(PAUSE_XS);
    console.log(`[rpa] logged-in: 勾选附加服务 side=${side} id=${id}`);
  }

  const closeBtn = page.getByRole("button", {
    name: /\d+\s+selected.*Chevron/i,
  });
  if ((await closeBtn.count()) > 0 && (await closeBtn.first().isVisible())) {
    await closeBtn.first().click();
  } else {
    await page.keyboard.press("Escape");
  }
  await page.waitForTimeout(PAUSE_SM);
  console.log(
    `[rpa] logged-in: 完成附加服务 side=${side} ids=${wanted.join(",")}`,
  );
}

/** YYYY-MM-DD 鈫?鏃ュ巻鏍煎瓙鍚嶃€孴hu Jul 16銆嶏紙褰曞埗 gridcell锛屾棤搴忔暟璇嶏級 */
export function formatLoggedInReadyGridCell(isoDate: string): string {
  const d = new Date(`${isoDate}T12:00:00`);
  if (Number.isNaN(d.getTime())) {
    return isoDate;
  }
  const weekday = d.toLocaleDateString("en-US", { weekday: "short" });
  const month = d.toLocaleDateString("en-US", { month: "short" });
  return `${weekday} ${month} ${d.getDate()}`;
}

/** 鎵撳紑鏃ュ巻鍚庣偣鐩爣鏃ワ細瀹樼綉鏃ユ牸甯镐綆瀵规瘮搴︼紝wait visible 浼氳秴鏃讹紝闇€ force */
async function clickReadyDateInCalendar(
  page: Page,
  isoDate: string,
): Promise<void> {
  const d = new Date(`${isoDate}T12:00:00`);
  if (Number.isNaN(d.getTime())) {
    throw new RpaError("RPA_DATA_INVALID", `可提货日无效: ${isoDate}`, {
      retryable: false,
    });
  }
  const day = d.getDate();
  const cellName = formatLoggedInReadyGridCell(isoDate);
  const monthLong = d.toLocaleDateString("en-US", { month: "long" });
  const year = d.getFullYear();

  // 鍒楀嚭褰撳墠鏃ュ巻鏍硷紝渚夸簬鎺掓煡鍚嶇О婕傜Щ
  const named = await page
    .locator('[role="gridcell"], [role="grid"] button')
    .evaluateAll((els) =>
      els.slice(0, 40).map((el) => ({
        role: el.getAttribute("role"),
        name: (el.getAttribute("aria-label") || el.textContent || "")
          .replace(/\s+/g, " ")
          .trim()
          .slice(0, 80),
        disabled:
          el.getAttribute("aria-disabled") === "true" ||
          (el as HTMLButtonElement).disabled === true,
      })),
    )
    .catch(() => [] as Array<{ role: string | null; name: string; disabled: boolean }>);
  if (named.length) {
    console.log(
      `[rpa] logged-in: 鏃ュ巻鏍奸噰鏍?${JSON.stringify(named.filter((n) => n.name).slice(0, 12))}`,
    );
  }

  const candidates = [
    page.getByRole("gridcell", { name: cellName }),
    page.getByRole("gridcell", {
      name: new RegExp(
        `${cellName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`,
        "i",
      ),
    }),
    page.getByRole("button", {
      name: new RegExp(`${monthLong}\\s+${day}(st|nd|rd|th)?,?\\s*${year}`, "i"),
    }),
    page.getByRole("gridcell", {
      name: new RegExp(`\\b${day}(st|nd|rd|th)?\\b`),
    }),
    page.getByRole("button", { name: new RegExp(`^${day}$`) }),
  ];

  for (const loc of candidates) {
    const n = await loc.count();
    for (let i = 0; i < Math.min(n, 8); i += 1) {
      const el = loc.nth(i);
      const attached = await el
        .waitFor({ state: "attached", timeout: 1_500 })
        .then(() => true)
        .catch(() => false);
      if (!attached) continue;
      const disabled =
        (await el.getAttribute("aria-disabled").catch(() => null)) === "true" ||
        (await el.isDisabled().catch(() => false));
      if (disabled) continue;
      const label = (
        (await el.getAttribute("aria-label").catch(() => null)) ||
        (await el.innerText().catch(() => "")) ||
        ""
      ).replace(/\s+/g, " ");
      // 官网禁选周末：跳过 Sat/Sun 格子
      if (/\b(Sat|Sun)\b/i.test(label)) continue;
      // 鏃ュ彿鍖归厤鏃堕伩鍏嶈鐐瑰叾瀹冩湀娈嬬暀鏍硷細浼樺厛鍚洰鏍囨棩鏂囨
      if (
        !label.includes(String(day)) &&
        !new RegExp(`\\b${day}\\b`).test(label) &&
        label !== String(day)
      ) {
        continue;
      }
      await el.click({ force: true });
      await page.waitForTimeout(PAUSE_SM);
      console.log(
        "[rpa] logged-in: selected ready-date " +
          isoDate +
          " via=" +
          JSON.stringify(label || cellName),
      );
      return;
    }
  }

  throw new RpaError(
    "RPA_DATA_INVALID",
    "登录态日历未点到 " +
      isoDate +
      " want=" +
      cellName +
      " sample=" +
      named
        .map((x) => x.name)
        .filter(Boolean)
        .slice(0, 8)
        .join("|"),
    { retryable: true },
  );
}

/**
 * Ready date/time dropdown helpers (recording 140935).
 * Time trigger a11y name is the CURRENT truncated value (e.g. ":00 AM"),
 * not the target — do not filter by target AM/PM (hits notification items).
 */
async function openReadyTimeDropdown(page: Page): Promise<void> {
  const afterLabel = page.getByText("after", { exact: true });
  if ((await afterLabel.count()) > 0) {
    await afterLabel.first().click().catch(() => undefined);
    await page.waitForTimeout(150);
  }

  const buttons = page.getByRole("button", {
    name: /:\d{2}\s*(AM|PM)/i,
  });
  const n = await buttons.count();
  for (let i = 0; i < n; i += 1) {
    const btn = buttons.nth(i);
    const testId = (await btn.getAttribute("data-testid").catch(() => null)) ?? "";
    if (testId.startsWith("notification-") || testId.includes("notification")) {
      continue;
    }
    const label = (
      (await btn.getAttribute("aria-label").catch(() => null)) ||
      (await btn.innerText().catch(() => "")) ||
      ""
    ).replace(/\s+/g, " ");
    // 鎷掓帀閫氱煡鏂囨锛涘彧瑕佸儚銆?:00 AM銆?銆?00 AM銆嶇殑鐭椂鍒婚挳
    if (/reschedule|shipment|notification/i.test(label)) continue;
    if (!/:\d{2}\s*(AM|PM)/i.test(label) && !/^\s*\d{1,2}:\d{2}\s*(AM|PM)\s*$/i.test(label)) {
      continue;
    }
    if (!(await btn.isVisible().catch(() => false))) continue;
    await btn.click();
    console.log(
      "[rpa] logged-in: open ready-time trigger=" + JSON.stringify(label.slice(0, 40)),
    );
    return;
  }

  // 鍏滃簳锛歛fter 鍚庣殑绗竴涓煭鏃跺埢鎸夐挳
  const nearby = page
    .locator("button")
    .filter({ hasText: /^\s*\d{1,2}:\d{2}\s*(AM|PM)\s*$/i });
  const nearbyCount = await nearby.count();
  for (let i = 0; i < nearbyCount; i += 1) {
    const btn = nearby.nth(i);
    const testId = (await btn.getAttribute("data-testid").catch(() => null)) ?? "";
    if (testId.startsWith("notification-")) continue;
    if (!(await btn.isVisible().catch(() => false))) continue;
    await btn.click();
    console.log("[rpa] logged-in: step");
    return;
  }

  throw new RpaError(
    "RPA_DATA_INVALID",
    "登录态未找到 Ready for pick-up 时刻触发钮",
    { retryable: true },
  );
}

async function applyReadyDateTime(
  page: Page,
  readyDate: string | undefined,
  readyTime: string | undefined,
): Promise<void> {
  if (readyDate?.trim()) {
    let iso = normalizeMothershipReadyDateIso(readyDate.trim()) ?? readyDate.trim();
    if (iso !== readyDate.trim()) {
      console.log(
        `[rpa] logged-in: ready_date weekend ${readyDate.trim()} → weekday ${iso}`,
      );
    }
    const dateBtn = page.getByRole("button", {
      name: /(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s*(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+\d{1,2}(st|nd|rd|th)?,?\s*20\d{2}.*Chevron/i,
    });
    await dateBtn.first().waitFor({ state: "visible", timeout: FIELD_WAIT_MS });
    await dateBtn.first().click();
    await page.waitForTimeout(PAUSE_MD);
    await clickReadyDateInCalendar(page, iso);
  } else {
    console.log("[rpa] logged-in: skip ready_date (none in request)");
  }

  if (readyTime?.trim()) {
    const timeLabel = readyTime
      .trim()
      .replace(/\uFF1A/g, ":")
      .replace(/\s+/g, " ")
      .replace(/([AP])\s*M/i, (_: string, ap: string) => ap.toUpperCase() + "M");

    await openReadyTimeDropdown(page);
    await page.waitForTimeout(PAUSE_SM);

    const optionExact = page.getByRole("option", {
      name: timeLabel,
      exact: true,
    });
    if ((await optionExact.count()) > 0) {
      await optionExact.first().click();
    } else {
      const escaped = timeLabel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
      const optionLoose = page.getByRole("option", {
        name: new RegExp("^\\s*" + escaped + "\\s*$", "i"),
      });
      await optionLoose.first().waitFor({ state: "visible", timeout: 8_000 });
      await optionLoose.first().click();
    }
    await page.waitForTimeout(PAUSE_XS);
    console.log(
      "[rpa] logged-in: selected ready-time option=" + JSON.stringify(timeLabel),
    );
  } else {
    console.log("[rpa] logged-in: skip ready_time (none in request)");
  }
}

/** 鐧诲綍鎬?cargo type id 鈫?瀹樼綉涓嬫媺鑻辨枃鍚嶏紙褰曞埗閫?Pallet/Box锛?*/
const MS_CARGO_TYPE_EN: Record<string, string> = {
  pallet: "Pallet",
  box: "Box",
  crate: "Crate",
  piece: "Piece",
  bale: "Bale",
  bucket: "Bucket",
  carton: "Carton",
  case: "Case",
  coil: "Coil",
  cylinder: "Cylinder",
  drum: "Drum",
  pail: "Pail",
  reel: "Reel",
  roll: "Roll",
  skid: "Skid",
  tote: "Tote",
  tube: "Tube",
};

async function selectCargoTypeAt(
  page: Page,
  index: number,
  cargoTypeId: string,
): Promise<void> {
  const label = MS_CARGO_TYPE_EN[cargoTypeId] ?? "Pallet";
  const typeInput = page.getByTestId("cargo-type-dropdown-input").nth(index);
  await typeInput.waitFor({ state: "visible", timeout: FIELD_WAIT_MS });
  await typeInput.click();
  await page.waitForTimeout(PAUSE_SM);

  const option = page.getByRole("option", {
    name: new RegExp(`^${label}$`, "i"),
  });
  if ((await option.count()) > 0) {
    await option.first().click();
  } else {
    // 褰曞埗锛歭ocator('div').filter({ hasText: /^Pallet$/ }).nth(4)
    const divOpt = page.locator("div").filter({ hasText: new RegExp(`^${label}$`) });
    const n = await divOpt.count();
    await divOpt.nth(Math.min(Math.max(n - 1, 0), 4)).click();
  }
  await page.waitForTimeout(PAUSE_XS);
  console.log("[rpa] logged-in: cargo type row=" + index + " type=" + label);
}

/** Multi-row cargo: fill row0 then cargo-add-button for more rows. */
async function fillCargo(page: Page, req: QuoteRequest): Promise<void> {
  const lines =
    req.cargoLines && req.cargoLines.length > 0
      ? req.cargoLines
      : [
          {
            cargoType: "pallet",
            quantity: req.palletCount || 1,
            weightLb: req.weightLb,
            lengthIn: req.dimsIn.l,
            widthIn: req.dimsIn.w,
            heightIn: req.dimsIn.h,
          },
        ];

  console.log(
    `[rpa] logged-in: 濉揣鐗╄鏁?${lines.length} types=${lines.map((l) => l.cargoType).join(",")}`,
  );

  for (let i = 0; i < lines.length; i += 1) {
    if (i > 0) {
      const addBtn = page.getByTestId("cargo-add-button");
      await addBtn.waitFor({ state: "visible", timeout: FIELD_WAIT_MS });
      await addBtn.click();
      await page.waitForTimeout(PAUSE_MD);
      console.log("[rpa] logged-in: step");
    }

    const line = lines[i]!;
    if (!isMothershipWeightEachAllowed(line.weightLb)) {
      throw new RpaError(
        "RPA_DATA_INVALID",
        `登录态单件重量不得超过 ${MOTHERSHIP_MAX_WEIGHT_EACH_LB} lb（当前 ${line.weightLb}）`,
        { retryable: false },
      );
    }
    await selectCargoTypeAt(page, i, line.cargoType);

    await page
      .getByTestId("cargo-quantity-input")
      .nth(i)
      .fill(String(Math.max(1, Math.round(line.quantity))));
    await page
      .getByTestId("cargo-weight-input")
      .nth(i)
      .fill(String(Math.round(line.weightLb)));
    await page
      .getByTestId("cargo-length-input")
      .nth(i)
      .fill(String(Math.round(line.lengthIn)));
    await page
      .getByTestId("cargo-width-input")
      .nth(i)
      .fill(String(Math.round(line.widthIn)));
    await page
      .getByTestId("cargo-height-input")
      .nth(i)
      .fill(String(Math.round(line.heightIn)));

    console.log(
      `[rpa] logged-in: 宸插～璐х墿 row=${i} qty=${line.quantity} wt=${line.weightLb} dims=${line.lengthIn}x${line.widthIn}x${line.heightIn}`,
    );
  }
}

async function fillLabeledTextNth(
  page: Page,
  label: RegExp,
  index: number,
  value: string | undefined,
): Promise<void> {
  const text = (value ?? "").trim();
  if (!text) return;
  const boxes = page.getByRole("textbox", { name: label });
  const loc = (await boxes.count()) > index ? boxes : page.getByLabel(label);
  if ((await loc.count()) <= index) return;
  const target = loc.nth(index);
  const current = (await target.inputValue().catch(() => "")).trim();
  if (current) return;
  await target.click({ force: true }).catch(() => undefined);
  await target.fill(text).catch(() => undefined);
}

/** MotherShip Details 下拉是 combobox/button，不是 native select */
async function selectLabeledOptionNth(
  page: Page,
  label: RegExp,
  index: number,
  optionText: string | undefined,
): Promise<void> {
  const want = (optionText ?? "").trim();
  if (!want) return;

  const combos = page.getByRole("combobox", { name: label });
  const buttons = page.getByRole("button", { name: label });
  const labeled = page.getByLabel(label);
  let target =
    (await combos.count()) > index
      ? combos.nth(index)
      : (await buttons.count()) > index
        ? buttons.nth(index)
        : (await labeled.count()) > index
          ? labeled.nth(index)
          : null;
  if (!target) return;

  const shown = (
    (await target.innerText().catch(() => "")) ||
    (await target.inputValue().catch(() => "")) ||
    ""
  )
    .replace(/\s+/g, " ")
    .trim();
  if (shown && !/^select(\s+time)?$/i.test(shown) && shown.toLowerCase().includes(want.toLowerCase().slice(0, 4))) {
    return;
  }

  // native <select> 兜底
  const tag = await target.evaluate((el) => el.tagName).catch(() => "");
  if (tag === "SELECT") {
    await target.selectOption({ label: want }).catch(() =>
      target!.selectOption({ index: 1 }),
    );
    return;
  }

  await target.click({ force: true }).catch(() => undefined);
  await page.waitForTimeout(PAUSE_SM);
  const escaped = want.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  const exact = page.getByRole("option", { name: want, exact: true });
  if ((await exact.count()) > 0) {
    await exact.first().click();
  } else {
    const loose = page.getByRole("option", {
      name: new RegExp("^\\s*" + escaped + "\\s*$", "i"),
    });
    if ((await loose.count()) > 0) {
      await loose.first().click();
    } else {
      const any = page.getByRole("option");
      const n = await any.count();
      for (let i = 0; i < n; i += 1) {
        const t = ((await any.nth(i).innerText().catch(() => "")) || "").trim();
        if (t && !/^select/i.test(t)) {
          await any.nth(i).click().catch(() => undefined);
          break;
        }
      }
    }
  }
  await page.keyboard.press("Escape").catch(() => undefined);
  await page.waitForTimeout(PAUSE_XS);
}

async function patchDetailsRequiredFields(
  page: Page,
  req: QuoteRequest,
): Promise<boolean> {
  if (!(await shouldPatchDetailsRequired(page))) {
    return false;
  }
  const details = req.mothershipDetails;
  const pieceQty = String(
    details?.cargo?.[0]?.piece_count_qty ??
      req.cargoLines?.[0]?.quantity ??
      req.palletCount ??
      1,
  );

  await fillLabeledTextNth(
    page,
    /Full company name/i,
    0,
    details?.pickup?.company_name || "Demo Pickup Co",
  );
  await fillLabeledTextNth(
    page,
    /Full company name/i,
    1,
    details?.delivery?.company_name || "Demo Delivery Co",
  );
  // 为何分两个邮箱：同邮箱会弹 Duplicate email，可能挡出价
  await fillLabeledTextNth(
    page,
    /On-site contact email/i,
    0,
    details?.pickup?.contact_email || "pickup-ops@example.com",
  );
  await fillLabeledTextNth(
    page,
    /On-site contact email/i,
    1,
    details?.delivery?.contact_email || "delivery-ops@example.com",
  );
  await fillLabeledTextNth(
    page,
    /On-site contact phone/i,
    0,
    details?.pickup?.contact_phone || "5555550101",
  );
  await fillLabeledTextNth(
    page,
    /On-site contact phone/i,
    1,
    details?.delivery?.contact_phone || "5555550102",
  );
  await selectLabeledOptionNth(
    page,
    /Opens at/i,
    0,
    details?.pickup?.opens_at || "8:00 AM",
  );
  await selectLabeledOptionNth(
    page,
    /Closes at/i,
    0,
    details?.pickup?.closes_at || "5:00 PM",
  );
  await selectLabeledOptionNth(
    page,
    /Opens at/i,
    1,
    details?.delivery?.opens_at || "8:00 AM",
  );
  await selectLabeledOptionNth(
    page,
    /Closes at/i,
    1,
    details?.delivery?.closes_at || "5:00 PM",
  );
  await selectLabeledOptionNth(
    page,
    /Piece count type/i,
    0,
    details?.cargo?.[0]?.piece_count_type || "Pieces",
  );
  await fillLabeledTextNth(page, /Piece count quantity/i, 0, pieceQty);
  await fillLabeledTextNth(
    page,
    /Cargo description/i,
    0,
    details?.cargo?.[0]?.description || "General freight",
  );
  console.log("[rpa] logged-in: patched details required fields");
  return true;
}

/** Inbox Got it：整次会话最多点 3 次，避免报价页死循环重试 */
let inboxGotItClicks = 0;
async function dismissInboxOnboarding(page: Page): Promise<void> {
  if (inboxGotItClicks >= 3) return;
  const gotIt = page.getByRole("button", { name: /^Got it$/i });
  if ((await gotIt.count()) === 0) return;
  const btn = gotIt.first();
  if (!(await btn.isVisible().catch(() => false))) return;
  inboxGotItClicks += 1;
  console.log("[rpa] logged-in: close Inbox Got it #" + String(inboxGotItClicks));
  await btn.click({ timeout: 2_000, force: true }).catch(() => undefined);
  await page.waitForTimeout(PAUSE_SM);
}

/** Inbox 抽屉挡住表单时 isVisible/click 会空等默认 30s */
async function closeInboxDrawer(page: Page): Promise<void> {
  await dismissInboxOnboarding(page);
  const marker = page
    .getByText(
      /Mark all as read|Stay up-to-date on your shipments with Inbox/i,
    )
    .first();
  if (
    (await marker.count()) === 0 ||
    !(await marker.isVisible().catch(() => false))
  ) {
    return;
  }

  console.log("[rpa] logged-in: close Inbox drawer Esc");
  await page.keyboard.press("Escape").catch(() => undefined);
  await page.waitForTimeout(PAUSE_SM);
  await dismissInboxOnboarding(page);

  if (await marker.isVisible().catch(() => false)) {
    // 点表单区域收起抽屉，避免点到导航重置
    await page.mouse.click(360, 280).catch(() => undefined);
    await page.waitForTimeout(PAUSE_XS);
    await page.keyboard.press("Escape").catch(() => undefined);
  }
}

/** 濉〃闃舵杞婚噺娓呴殰锛氬嬁鐐瑰鑸紝閬垮厤鎵撲贡鍦板潃/璐х墿琛ㄥ崟 */
async function dismissFormOverlays(page: Page): Promise<void> {
  await dismissGlobalShipmentSearchModal(page);
  await dismissInboxOnboarding(page);
  const drawerOpen = page.getByText(/Mark all as read/i);
  if (
    (await drawerOpen.count()) > 0 &&
    (await drawerOpen.first().isVisible().catch(() => false))
  ) {
    await page.keyboard.press("Escape").catch(() => undefined);
    await page.waitForTimeout(PAUSE_XS);
  }
}

/** Continue 鍚庨噸娓呴殰锛欼nbox 鎶藉眽 / 鍏ㄥ眬鎼滅储 / 娈嬬暀寮瑰眰 */
async function dismissBlockingOverlays(page: Page): Promise<void> {
  await dismissGlobalShipmentSearchModal(page);
  await closeInboxDrawer(page);
  await page.keyboard.press("Escape").catch(() => undefined);
  await page.waitForTimeout(PAUSE_XS);
}

async function readPortalQuoteBlockMessage(
  page: Page,
): Promise<string | null> {
  const body = await page.locator("body").innerText().catch(() => "");
  return parseMothershipPortalQuoteMessageFromBody(body);
}

async function readPortalAccessorialBlockMessage(
  page: Page,
): Promise<string | null> {
  const body = await page.locator("body").innerText().catch(() => "");
  const msgs = extractMothershipAccessorialBlockMessages(body);
  return formatMothershipPortalQuoteMessage(msgs);
}

/** 官网业务拒价：如实抛出给前端，非系统超时 */
async function throwIfPortalAccessorialBlocked(page: Page): Promise<void> {
  if (await isQuotesLoading(page)) return;
  const msg = await readPortalAccessorialBlockMessage(page);
  if (!msg) return;
  console.log("[rpa] logged-in: portal accessorial blocked: " + msg.slice(0, 200));
  throw new RpaError("CARRIER_NO_CAPACITY", msg, { retryable: false });
}

async function throwIfPortalQuoteBlocked(
  page: Page,
): Promise<void> {
  if (await isQuotesLoading(page)) return;
  const msg = await readPortalQuoteBlockMessage(page);
  if (!msg) return;
  console.log("[rpa] logged-in: portal quote blocked: " + msg.slice(0, 200));
  throw new RpaError("CARRIER_NO_CAPACITY", msg, { retryable: false });
}
async function isQuotesLoading(page: Page): Promise<boolean> {
  const markers = [
    page.getByText(/loading rates/i),
    page.getByText(/getting (your )?quotes?/i),
    page.getByText(/calculating/i),
    page.getByText(/fetching rates/i),
    page.getByText(/updating quote/i),
    page.locator('[role="progressbar"]'),
    page.locator('[aria-busy="true"]'),
  ];
  for (const m of markers) {
    if ((await m.count()) > 0 && (await m.first().isVisible().catch(() => false))) {
      return true;
    }
  }
  return false;
}

async function hasRateCardsOrOptions(page: Page): Promise<boolean> {
  const rateCount = await page.getByTestId("rate-card").count().catch(() => 0);
  if (rateCount > 0) return true;
  const optionCount = await page
    .locator('[data-testid^="quote-details-rate-option-"]')
    .count()
    .catch(() => 0);
  return optionCount > 0;
}

/** Continue 后 grace 内只等一级自然出价，不碰二级 Details */
async function waitForRatesAfterContinue(page: Page): Promise<boolean> {
  const deadline = Date.now() + RATE_GRACE_AFTER_CONTINUE_MS;
  let overlayRounds = 0;
  while (Date.now() < deadline) {
    if (overlayRounds < 3) {
      await dismissBlockingOverlays(page);
      overlayRounds += 1;
    }
    if (await hasRateCardsOrOptions(page)) {
      const n = await page.getByTestId("rate-card").count().catch(() => 0);
      console.log(
        "[rpa] logged-in: rate-card after Continue grace count=" + String(n),
      );
      return true;
    }
    if (await isQuotesLoading(page)) {
      console.log("[rpa] logged-in: rates loading after Continue, waiting...");
      await page.waitForTimeout(PAUSE_MD);
      continue;
    }
    await page.waitForTimeout(PAUSE_MD);
  }
  console.log("[rpa] logged-in: grace elapsed after Continue, still no rate-card");
  return false;
}

/** 仅非 loading 且仍被官网阻断时才补二级默认值 */
async function shouldPatchDetailsRequired(page: Page): Promise<boolean> {
  if (await hasRateCardsOrOptions(page)) return false;
  if (await isQuotesLoading(page)) return false;
  const body = await page.locator("body").innerText().catch(() => "");
  return /Please review all required fields to proceed/i.test(body);
}

async function clickSaveAndUpdateQuote(page: Page): Promise<boolean> {
  const saveUpdate = page.getByRole("button", {
    name: /Save\s*&\s*update quote/i,
  });
  if ((await saveUpdate.count()) === 0) return false;
  const btn = saveUpdate.first();
  if (!(await btn.isVisible().catch(() => false))) return false;
  if (await btn.isDisabled().catch(() => false)) {
    console.log("[rpa] logged-in: click Save & update quote");
    return false;
  }
  console.log("[rpa] logged-in: 鐐瑰嚮 Save & update quote 鍥炲埛鎶ヤ环");
  await btn.click();
  await page.waitForTimeout(PAUSE_MD);
  return true;
}

async function clickQuotesStep(page: Page): Promise<boolean> {
  // 仅允许点击货件流程内的 Quotes step，禁止误点左侧全局 Quotes 导航
  const stepper = page
    .locator("div, nav, header, main")
    .filter({ hasText: /Quotes[\s\S]{0,40}Details[\s\S]{0,40}Review/i })
    .first();
  if ((await stepper.count()) > 0) {
    const q = stepper.getByText(/^Quotes$/i);
    if ((await q.count()) > 0 && (await q.first().isVisible().catch(() => false))) {
      console.log("[rpa] logged-in: 鐐瑰嚮杩涘害鏉?Quotes");
      await q.first().click().catch(() => undefined);
      await page.waitForTimeout(PAUSE_MD);
      return true;
    }
  }
  return false;
}

/**
 * Continue 后可能落到：Quotes 加载中 / Details+Save&update / Inbox 遮挡 / 无运力。
 * 统一清障并驱动到可见 rate-card。
 */
async function ensureRateCardsReady(
  page: Page,
  req?: QuoteRequest,
): Promise<void> {
  await dismissBlockingOverlays(page);

  const deadline = Date.now() + RATE_WAIT_MS;
  let idleRounds = 0;
  let overlayRounds = 0;
  let requiredPatchRounds = 0;

  while (Date.now() < deadline) {
    // 遮罩清理限流：最多前 4 轮，避免报价页狂点 Got it
    if (overlayRounds < 4) {
      await dismissBlockingOverlays(page);
      overlayRounds += 1;
    } else if (overlayRounds === 4) {
      await page.keyboard.press("Escape").catch(() => undefined);
      overlayRounds += 1;
    }

    const rateCards = page.getByTestId("rate-card");
    const rateCount = await rateCards.count();
    // DOM 已有价卡即就绪（Inbox 遮罩时 isVisible 会假阴性）
    if (rateCount > 0) {
      if (!(await rateCards.first().isVisible().catch(() => false))) {
        await page.keyboard.press("Escape").catch(() => undefined);
        await page.waitForTimeout(PAUSE_XS);
      }
      console.log(
        "[rpa] logged-in: rate-card ready count=" + String(rateCount),
      );
      return;
    }

    const option = page
      .locator('[data-testid^="quote-details-rate-option-"]')
      .first();
    if ((await option.count()) > 0) {
      console.log("[rpa] logged-in: rate-option ready");
      return;
    }

    if (await isQuotesLoading(page)) {
      idleRounds = 0;
      console.log("[rpa] logged-in: rates loading...");
      await page.waitForTimeout(PAUSE_MD);
      continue;
    }

    await throwIfPortalAccessorialBlocked(page);

    if (
      req &&
      requiredPatchRounds < 2 &&
      (await shouldPatchDetailsRequired(page))
    ) {
      requiredPatchRounds += 1;
      console.log(
        "[rpa] logged-in: required fields still blocking, re-patch #" +
          String(requiredPatchRounds),
      );
      await patchDetailsRequiredFields(page, req);
      await clickSaveAndUpdateQuote(page);
      idleRounds = 0;
      await page.waitForTimeout(PAUSE_MD);
      continue;
    }

    await throwIfPortalQuoteBlocked(page);

    if (await clickSaveAndUpdateQuote(page)) {
      idleRounds = 0;
      await page.waitForTimeout(PAUSE_MD);
      continue;
    }

    idleRounds += 1;
    if (idleRounds === 3 || idleRounds === 8) {
      console.log(
        "[rpa] logged-in: still no rate-card idle=" +
          String(idleRounds) +
          " url=" +
          page.url(),
      );
    }
    await page.waitForTimeout(PAUSE_MD);
  }

  if (!(await isQuotesLoading(page))) {
    const portalMsg = await readPortalQuoteBlockMessage(page);
    if (portalMsg) {
      console.log(
        "[rpa] logged-in: portal quote blocked at timeout: " +
          portalMsg.slice(0, 200),
      );
      throw new RpaError("CARRIER_NO_CAPACITY", portalMsg, {
        retryable: false,
      });
    }
  }

  throw new RpaError(
    "PAGE_LOAD_TIMEOUT",
    "登录态等待 rate-card 超时（已尝试关 Inbox / Save&update / Quotes）",
    { retryable: true },
  );
}

async function scrapeRateCards(
  page: Page,
  req?: QuoteRequest,
): Promise<QuoteItem[]> {
  await ensureRateCardsReady(page, req);
  // 价卡已在 DOM：只清一次遮罩，不再每轮 dismiss
  await page.keyboard.press("Escape").catch(() => undefined);
  await dismissInboxOnboarding(page);

  const items: QuoteItem[] = [];
  for (let attempt = 0; attempt < 4; attempt += 1) {
    items.length = 0;

    const optionRoots = page.locator(
      '[data-testid^="quote-details-rate-option-"]',
    );
    const optionCount = await optionRoots.count();

    if (optionCount > 0) {
      for (let i = 0; i < optionCount; i += 1) {
        const text = await optionRoots.nth(i).innerText().catch(() => "");
        const parsed = parseLoggedInRateCardText(text);
        if (parsed) items.push(parsed);
      }
    } else {
      const cards = page.getByTestId("rate-card");
      const n = await cards.count();
      for (let i = 0; i < n; i += 1) {
        const text = await cards.nth(i).innerText().catch(() => "");
        const parsed = parseLoggedInRateCardText(text);
        if (parsed) items.push(parsed);
      }
    }

    if (items.length > 0) {
      console.log(
        "[rpa] logged-in: scraped rates=" +
          String(items.length) +
          " carriers=" +
          items.map((x) => x.carrier).join(","),
      );
      return items;
    }

    console.log(
      "[rpa] logged-in: rate-card text not parsed, retry " +
        String(attempt + 1) +
        "/4",
    );
    if (attempt === 1) {
      await clickSaveAndUpdateQuote(page);
    }
    await page.waitForTimeout(PAUSE_MD);
  }

  throw new RpaError(
    "RPA_DATA_INVALID",
    "登录态 rate-card 未能解析出承运商报价",
    { retryable: true },
  );
}

/** 鏈夎处瀵嗘椂锛歞ashboard Ship 鈫?Continue 鈫?鎶?rate-card */
export async function runMothershipLoggedInDashboardQuote(
  req: QuoteRequest,
): Promise<QuoteItem[]> {
  inboxGotItClicks = 0;
  if (!hasEffectiveMothershipLogin()) {
    throw new RpaError(
      "STRUCT_CHANGE",
      "runMothershipLoggedInDashboardQuote 需要 MotherShip 账密",
      { retryable: false },
    );
  }

  return withRpaSessionLock(async () => {
    const storagePath = resolveStorageStatePath();
    const headed = process.env.RPA_HEADED === "true";
    const headless = headed ? false : process.env.RPA_HEADLESS !== "false";
    const slowMoRaw = process.env.RPA_SLOW_MO_MS?.trim();
    const slowMo =
      !headless && slowMoRaw && Number.isFinite(Number(slowMoRaw))
        ? Number(slowMoRaw)
        : !headless
          ? 60
          : undefined;
    const browser = await launchRpaBrowser({
      headless,
      ...(slowMo !== undefined ? { slowMo } : {}),
    });
    const context = await createContext(browser, {
      storageStatePath: storagePath,
    });
    const page = await context.newPage();

    const t0 = Date.now();
    let lastAt = t0;
    let lastName = "launch";
    const step = (name: string) => {
      const now = Date.now();
      console.log(
        "[rpa] logged-in timing: step=\"" +
          lastName +
          "\" ms=" +
          String(now - lastAt) +
          " totalMs=" +
          String(now - t0),
      );
      console.log("[rpa] logged-in step: " + name + " url=" + page.url());
      lastName = name;
      lastAt = now;
    };
    const finishTiming = (label: string) => {
      const now = Date.now();
      console.log(
        "[rpa] logged-in timing: step=\"" +
          lastName +
          "\" ms=" +
          String(now - lastAt) +
          " totalMs=" +
          String(now - t0),
      );
      console.log(
        "[rpa] logged-in timing: DONE label=" +
          label +
          " totalMs=" +
          String(now - t0),
      );
    };

    try {
      console.log(
        `[rpa] logged-in dashboard quote start headed=${!headless} slowMo=${slowMo ?? 0} storage=${storagePath}`,
      );
      step("goto create shipment");
      await page.goto(MOTHERSHIP_CREATE_SHIPMENT_URL, {
        waitUntil: "domcontentloaded",
        timeout: 60_000,
      });
      step("loginIfNeeded");
      await loginDashboardIfNeeded(page);
      step("dismissGlobalSearch");
      await dismissGlobalShipmentSearchModal(page);
      step("openCreateShipment");
      await openCreateShipment(page);
      step(`pickup address: ${addressQuery(req.pickup).slice(0, 60)}`);
      await pickAddressSuggestion(page, "pickup", addressQuery(req.pickup));
      step(`delivery address: ${addressQuery(req.delivery).slice(0, 60)}`);
      await pickAddressSuggestion(page, "delivery", addressQuery(req.delivery));
      // 褰曞埗椤哄簭锛氬弻鍦板潃纭鍚?鈫?鎻愯揣闄勫姞 鈫?娲鹃€侀檮鍔?鈫?鏃ユ湡/鏃跺埢 鈫?cargo
      console.log(
        `[rpa] logged-in payload accessorials pickup=[${(req.pickupAccessorials ?? []).join(",")}] delivery=[${(req.deliveryAccessorials ?? []).join(",")}] ready=${req.readyDate ?? "-"} ${req.readyTime ?? "-"}`,
      );
      step(
        `pickup accessorials: ${(req.pickupAccessorials ?? []).join(",") || "(none)"}`,
      );
      await applyAccessorials(page, "pickup", req.pickupAccessorials);
      step(
        `delivery accessorials: ${(req.deliveryAccessorials ?? []).join(",") || "(none)"}`,
      );
      await applyAccessorials(page, "delivery", req.deliveryAccessorials);
      step(`ready datetime: ${req.readyDate ?? "-"} ${req.readyTime ?? "-"}`);
      await applyReadyDateTime(page, req.readyDate, req.readyTime);
      step("fillCargo");
      await fillCargo(page, req);
      step("click Continue");
      await dismissBlockingOverlays(page);

      // 最终校验：两个地址均已确认，否则立即报错（避免空等 rate-card 120s）
      {
        const pickupTokens = addressQuery(req.pickup)
          .split(/[,\s]+/)
          .filter((t) => t.length >= 3)
          .slice(0, 3);
        const deliveryTokens = addressQuery(req.delivery)
          .split(/[,\s]+/)
          .filter((t) => t.length >= 3)
          .slice(0, 3);
        const pickupOk = await isAddressConfirmed(page, "pickup", pickupTokens);
        const deliveryOk = await isAddressConfirmed(page, "delivery", deliveryTokens);
        if (!pickupOk || !deliveryOk) {
          const which = !pickupOk ? "pickup" : "delivery";
          throw new RpaError(
            "ADDRESS_SUGGESTION_NOT_FOUND",
            "点 Continue 前地址未确认（" + which + "），中止避免空等超时",
            { retryable: true },
          );
        }
      }

      const continueBtn = page.getByTestId("ship-create-continue-button");
      await continueBtn.waitFor({ state: "visible", timeout: FIELD_WAIT_MS });
      // 绛夊彲鐐癸紙鏍￠獙鏈畬鎴愭椂绂佺敤锛?
      for (let i = 0; i < 12; i += 1) {
        if (!(await continueBtn.isDisabled().catch(() => false))) break;
        await page.waitForTimeout(PAUSE_SM);
      }
      if (await continueBtn.isDisabled().catch(() => false)) {
        throw new RpaError(
          "RPA_DATA_INVALID",
          "登录态 Continue 按钮仍禁用（地址/货物可能未确认）",
          { retryable: true },
        );
      }
      await continueBtn.click();
      const navigated = await Promise.race([
        page
          .waitForURL(/\/ship\/single/i, { timeout: 45_000 })
          .then(() => true),
        page
          .getByTestId("rate-card")
          .first()
          .waitFor({ state: "visible", timeout: 45_000 })
          .then(() => true),
        page
          .getByRole("button", { name: /Save\s*&\s*update quote/i })
          .first()
          .waitFor({ state: "visible", timeout: 45_000 })
          .then(() => true),
      ]).catch(() => false);
      if (!navigated && /\/ship\/?$/i.test(page.url())) {
        await throwIfPortalQuoteBlocked(page);
        const body = await page.locator("body").innerText().catch(() => "");
        if (/Please enter the address of your delivery location/i.test(body)) {
          throw new RpaError(
            "ADDRESS_SUGGESTION_NOT_FOUND",
            "Continue 后仍停留在创建页且派送地址未确认，请重新选择派送地址",
            { retryable: true },
          );
        }
        if (/Please enter the address of your pick-?up location/i.test(body)) {
          throw new RpaError(
            "ADDRESS_SUGGESTION_NOT_FOUND",
            "Continue 后仍停留在创建页且提货地址未确认，请重新选择提货地址",
            { retryable: true },
          );
        }
      }
      step("wait initial rates after Continue");
      const gotRatesEarly = await waitForRatesAfterContinue(page);
      if (!gotRatesEarly) {
        await throwIfPortalAccessorialBlocked(page);
        if (await shouldPatchDetailsRequired(page)) {
          console.log(
            "[rpa] logged-in: grace ended still blocked, patch details fallback",
          );
          await patchDetailsRequiredFields(page, req);
          await clickSaveAndUpdateQuote(page);
          await page.waitForTimeout(PAUSE_SM);
        } else {
          await throwIfPortalQuoteBlocked(page);
        }
      } else {
        console.log("[rpa] logged-in: skip details patch (rates from level-1)");
      }
      step("wait rate-card / scrape");
      const raw = await scrapeRateCards(page, req);
      const items = normalizeQuoteItems(raw);
      await context.storageState({ path: storagePath }).catch(() => undefined);
      finishTiming("ok");
      console.log(
        `[rpa] logged-in dashboard quote ok carriers=${items
          .map((i) => i.carrier)
          .join(",")}`,
      );
      return items;
    } catch (err) {
      const brief = err instanceof Error ? err.message : String(err);
      console.error(
        `[rpa] logged-in FAILED at url=${page.url()} err=${brief.slice(0, 300)}`,
      );
      try {
        const diagDir = path.join(process.cwd(), ".rpa", "diag");
        fs.mkdirSync(diagDir, { recursive: true });
        const stamp = new Date().toISOString().replace(/[:.]/g, "-");
        const shot = path.join(diagDir, `logged-in-fail-${stamp}.png`);
        await page.screenshot({ path: shot, fullPage: true });
        const body = (
          await page.locator("body").innerText().catch(() => "")
        ).slice(0, 5000);
        const rateN = await page.getByTestId("rate-card").count().catch(() => -1);
        const continueN = await page
          .getByTestId("ship-create-continue-button")
          .count()
          .catch(() => -1);
        const continueDisabled = await page
          .getByTestId("ship-create-continue-button")
          .first()
          .isDisabled()
          .catch(() => null);
        const meta = {
          url: page.url(),
          err: brief.slice(0, 800),
          rateCardCount: rateN,
          continueCount: continueN,
          continueDisabled,
          bodyPreview: body,
        };
        const jsonPath = path.join(diagDir, `logged-in-fail-${stamp}.json`);
        fs.writeFileSync(jsonPath, JSON.stringify(meta, null, 2), "utf8");
        console.error(`[rpa] logged-in diag shot=${shot}`);
        console.error(`[rpa] logged-in diag json=${jsonPath}`);
        console.error(`[rpa] logged-in bodyPreview=\n${body.slice(0, 1500)}`);
      } catch (diagErr) {
        console.error(
          "[rpa] logged-in diag dump failed: " +
            (diagErr instanceof Error ? diagErr.message : String(diagErr)),
        );
      }
      throw err;
    } finally {
      // headed: brief pause so final screen is visible
      if (!headless) {
        await page.waitForTimeout(2_000).catch(() => undefined);
      }
      await context.close().catch(() => undefined);
      await browser.close().catch(() => undefined);
    }
  });
}
