import type { Locator, Page } from "playwright";
import type { QuoteRequest } from "@/modules/providers/quote-provider";
import { pageLocator } from "@/lib/rpa/locator";
import { clickFirstVisibleFromSpecs } from "@/lib/rpa/selector-specs";
import { RpaError } from "@/modules/rpa/errors";
import type { RPAContext } from "@/workers/rpa/kernel/context";
import type { AxelSelectionBridge } from "@/workers/rpa/axel-selection-bridge";
import {
  getAddressConfirmationSurfaceText,
  isAddressInputCommitted,
  isAddressSuggestionDropdownOpen,
  waitForAddressSuggestionLocked,
} from "@/workers/rpa/address-suggestions";
import { isPickupChipVisuallyReady } from "@/workers/rpa/address-chip-visual";
import { isBlindAddressFlowActive } from "@/lib/rpa/env";
import { mouseClickSuggestionUntilVisualConfirmed } from "@/workers/rpa/address-suggestion-mouse";
import { visibleStreetInput } from "@/workers/rpa/address-side";

type AddressFields = QuoteRequest["pickup"];

const POLL_MS = 200;
const ADDRESS_SIDE_COMMIT_TIMEOUT_MS = 12_000;
const ADDRESS_INPUT_STABLE_MS = 2_000;

export type AddressCommitSnapshot = {
  side: "pickup" | "delivery";
  inputValues: string[];
  surfaceText: string;
  widgetTextPreview: string;
  dropdownOpen: boolean;
  axelLocationHit: boolean;
};

function toQueryHint(address: AddressFields) {
  return {
    street: address.street,
    city: address.city ?? "",
    state: address.state ?? "",
    zip: address.zip ?? "",
  };
}

function normalizeWidgetText(text: string): string {
  return text
    .replace(/\bavenue\b/gi, "ave")
    .replace(/\bboulevard\b/gi, "blvd")
    .replace(/\bstreet\b/gi, "st")
    .replace(/\s+/g, " ")
    .trim()
    .toLowerCase();
}

function hasWordToken(text: string, token: string): boolean {
  const escaped = token.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  return new RegExp(`\\b${escaped}\\b`, "i").test(text);
}

const US_STATE_NAME_TO_ABBR: Record<string, string> = {
  alabama: "al",
  alaska: "ak",
  arizona: "az",
  arkansas: "ar",
  california: "ca",
  colorado: "co",
  connecticut: "ct",
  delaware: "de",
  florida: "fl",
  georgia: "ga",
  hawaii: "hi",
  idaho: "id",
  illinois: "il",
  indiana: "in",
  iowa: "ia",
  kansas: "ks",
  kentucky: "ky",
  louisiana: "la",
  maine: "me",
  maryland: "md",
  massachusetts: "ma",
  michigan: "mi",
  minnesota: "mn",
  mississippi: "ms",
  missouri: "mo",
  montana: "mt",
  nebraska: "ne",
  nevada: "nv",
  "new hampshire": "nh",
  "new jersey": "nj",
  "new mexico": "nm",
  "new york": "ny",
  "north carolina": "nc",
  "north dakota": "nd",
  ohio: "oh",
  oklahoma: "ok",
  oregon: "or",
  pennsylvania: "pa",
  "rhode island": "ri",
  "south carolina": "sc",
  "south dakota": "sd",
  tennessee: "tn",
  texas: "tx",
  utah: "ut",
  vermont: "vt",
  virginia: "va",
  washington: "wa",
  "west virginia": "wv",
  wisconsin: "wi",
  wyoming: "wy",
};

function stateAppearsInWidget(widgetNorm: string, state: string): boolean {
  const raw = state.trim().toLowerCase();
  if (!raw) {
    return true;
  }
  if (widgetNorm.includes(raw)) {
    return true;
  }
  if (raw.length === 2 && widgetNorm.includes(`, ${raw}`)) {
    return true;
  }
  const abbr = US_STATE_NAME_TO_ABBR[raw];
  if (abbr && widgetNorm.includes(`, ${abbr}`)) {
    return true;
  }
  return false;
}

/** widget 文案是否出现派送侧占位（chip 态：Deliver to / Where to?） */
export function widgetShowsDeliverySectionCue(text: string): boolean {
  const normalized = text.replace(/\s+/g, " ");
  return /(?:^|\s)(Deliver to|Delivery to|Where to\?)(?:\s|$)/i.test(
    normalized,
  );
}

/** widget 内是否体现用户确认的 MotherShip 地址（防 widget 回退） */
export function widgetContainsConfirmedAddress(
  widgetText: string,
  address: AddressFields,
): boolean {
  const label = address.mothershipDisplayLabel?.trim();
  if (!label) {
    return false;
  }
  const w = normalizeWidgetText(widgetText);
  const streetNum = address.street.match(/^(\d+)/)?.[1];
  const labelNorm = normalizeWidgetText(label);
  if (
    streetNum &&
    labelNorm.includes(streetNum.toLowerCase()) &&
    !w.includes(streetNum.toLowerCase())
  ) {
    return false;
  }
  const state = address.state?.trim();
  if (state && !stateAppearsInWidget(w, state)) {
    return false;
  }
  const city = address.city?.trim().toLowerCase();
  if (city && city.length >= 3 && !w.includes(city)) {
    const cityParts = city.split(/\s+/).filter((part) => part.length >= 2);
    if (cityParts.length > 1) {
      const allPartsPresent = cityParts.every((part) =>
        hasWordToken(widgetText, part),
      );
      if (!allPartsPresent) {
        return false;
      }
    } else {
      const streetCore = normalizeWidgetText(address.street);
      if (!streetCore || !w.includes(streetCore)) {
        return false;
      }
    }
  }
  const labelTokens = label
    .replace(/([a-z])([A-Z])/g, "$1 $2")
    .split(/[,\s]+/)
    .map((t) => t.toLowerCase())
    .filter((t) => t.length > 3);
  const streetCore = normalizeWidgetText(address.street);
  if (
    streetCore &&
    !/\bbridge\b/.test(streetCore) &&
    /\bbridge\b/.test(w) &&
    w.includes(streetCore.split(/\s+/)[0] ?? "")
  ) {
    return false;
  }
  return labelTokens.some((t) => w.includes(t));
}


/** 提货 chip 视觉确认 + Deliver to 占位（委托 address-chip-visual） */
export async function isPickupChipCommitted(
  page: Page,
  widgetSelector: string,
  pickup: AddressFields,
): Promise<boolean> {
  return isPickupChipVisuallyReady(page, widgetSelector, pickup);
}

async function readCommittedInputSurface(
  page: Page,
  widgetSelector: string,
): Promise<string> {
  return page.evaluate(
    `(function(sel) {
      var widget = document.querySelector(sel);
      if (!widget) return "";
      var chunks = [];
      var inputs = widget.querySelectorAll("input");
      for (var i = 0; i < inputs.length; i++) {
        var val = (inputs[i].value || "").replace(/\\s+/g, " ").trim();
        if (val.length >= 8) chunks.push(val);
      }
      return chunks.join(" ");
    })(${JSON.stringify(widgetSelector)})`,
  ) as Promise<string>;
}

async function readStreetInputValues(
  page: Page,
  widgetSelector: string,
): Promise<string[]> {
  return page.evaluate(
    `(function(sel) {
      var widget = document.querySelector(sel);
      if (!widget) return [];
      var out = [];
      var inputs = widget.querySelectorAll("input");
      for (var i = 0; i < inputs.length; i++) {
        var ph = (inputs[i].placeholder || "").toLowerCase();
        if (!/search|搜索/.test(ph)) continue;
        var val = (inputs[i].value || "").replace(/\\s+/g, " ").trim();
        if (val) out.push(val);
      }
      return out;
    })(${JSON.stringify(widgetSelector)})`,
  ) as Promise<string[]>;
}

export async function readWidgetCombinedText(
  page: Page,
  widgetSelector: string,
): Promise<string> {
  const surfaceText = await getAddressConfirmationSurfaceText(
    page,
    widgetSelector,
  );
  const fullText = await pageLocator(page, widgetSelector)
    .innerText()
    .catch(() => "");
  return [surfaceText, fullText]
    .map((part) => part.replace(/\s+/g, " ").trim())
    .filter(Boolean)
    .join(" ");
}

export async function captureAddressCommitSnapshot(
  ctx: RPAContext,
  side: "pickup" | "delivery",
  widgetSelector: string,
  axelLocationHit = false,
): Promise<AddressCommitSnapshot> {
  const widgetText = await readWidgetCombinedText(ctx.page, widgetSelector);
  return {
    side,
    inputValues: await readStreetInputValues(ctx.page, widgetSelector),
    surfaceText: await getAddressConfirmationSurfaceText(
      ctx.page,
      widgetSelector,
    ),
    widgetTextPreview: widgetText.slice(0, 220),
    dropdownOpen: await isAddressSuggestionDropdownOpen(ctx.page),
    axelLocationHit,
  };
}

export function logAddressCommitSnapshot(
  phase: string,
  snapshot: AddressCommitSnapshot,
): void {
  console.log(
    `[rpa] address-commit:${phase} side=${snapshot.side} inputs=${JSON.stringify(snapshot.inputValues)} dropdown=${snapshot.dropdownOpen} axelLocation=${snapshot.axelLocationHit} widget=${snapshot.widgetTextPreview}`,
  );
}

export async function dismissAddressSuggestionDropdown(
  page: Page,
  streetInput?: Locator | null,
): Promise<boolean> {
  const deadline = Date.now() + 4_000;
  while (Date.now() < deadline) {
    if (!(await isAddressSuggestionDropdownOpen(page))) {
      return true;
    }
    if (streetInput) {
      await streetInput.press("Enter").catch(() => undefined);
    }
    await page.keyboard.press("Escape").catch(() => undefined);
    await page.keyboard.press("Tab").catch(() => undefined);
    await page.waitForTimeout(POLL_MS);
  }
  return !(await isAddressSuggestionDropdownOpen(page));
}

/** 强制收起联想（仅键盘，避免误点 Use my location / 重置 widget） */
export async function forceDismissAddressSuggestionDropdown(
  page: Page,
  _widgetSelector: string,
): Promise<void> {
  for (let i = 0; i < 8; i += 1) {
    if (!(await isAddressSuggestionDropdownOpen(page))) {
      return;
    }
    await page.keyboard.press("Escape").catch(() => undefined);
    await page.waitForTimeout(POLL_MS);
  }
  for (let i = 0; i < 3; i += 1) {
    await page.keyboard.press("Tab").catch(() => undefined);
    await page.waitForTimeout(POLL_MS);
    if (!(await isAddressSuggestionDropdownOpen(page))) {
      return;
    }
  }
}

async function readAddressSurfaceForCommit(
  page: Page,
  widgetSelector: string,
  dropdownOpen: boolean,
): Promise<string> {
  const surfaceText = await getAddressConfirmationSurfaceText(
    page,
    widgetSelector,
  );
  if (dropdownOpen) {
    return surfaceText;
  }
  return readWidgetCombinedText(page, widgetSelector);
}

async function waitForInputSurfaceStable(
  page: Page,
  widgetSelector: string,
  address: AddressFields,
  stableMs: number,
  timeoutMs: number,
): Promise<boolean> {
  const deadline = Date.now() + timeoutMs;
  let lastSurface = "";
  let stableSince = 0;

  while (Date.now() < deadline) {
    const surface = await getAddressConfirmationSurfaceText(page, widgetSelector);
    const now = Date.now();
    if (widgetContainsConfirmedAddress(surface, address)) {
      if (surface === lastSurface) {
        if (stableSince === 0) {
          stableSince = now;
        }
        if (now - stableSince >= stableMs) {
          return true;
        }
      } else {
        lastSurface = surface;
        stableSince = now;
      }
    } else {
      lastSurface = "";
      stableSince = 0;
    }
    await page.waitForTimeout(POLL_MS);
  }

  return false;
}

async function waitForInputValueStable(
  input: Locator,
  address: AddressFields,
  stableMs: number,
  timeoutMs: number,
): Promise<boolean> {
  const queryHint = toQueryHint(address);
  const deadline = Date.now() + timeoutMs;
  let lastValue = "";
  let stableSince = 0;

  while (Date.now() < deadline) {
    const value = ((await input.inputValue().catch(() => "")) || "")
      .replace(/\s+/g, " ")
      .trim();
    const committed = await isAddressInputCommitted(input, queryHint);
    const now = Date.now();

    if (value.length >= 8 && committed && value === lastValue) {
      if (stableSince === 0) {
        stableSince = now;
      }
      if (now - stableSince >= stableMs) {
        return true;
      }
    } else {
      lastValue = value;
      stableSince = committed && value.length >= 8 ? now : 0;
    }

    await input.page().waitForTimeout(POLL_MS);
  }

  return false;
}

/** 收起当前地址编辑焦点，避免双地址 focus 竞争 */
export async function blurAddressEditing(
  ctx: RPAContext,
  widgetSelector: string,
): Promise<void> {
  const page = ctx.page;
  await page.keyboard.press("Escape").catch(() => undefined);
  await page.keyboard.press("Tab").catch(() => undefined);

  const deadline = Date.now() + 3_000;
  while (Date.now() < deadline) {
    if (!(await isAddressSuggestionDropdownOpen(page))) {
      return;
    }
    await page.keyboard.press("Escape").catch(() => undefined);
    await page.waitForTimeout(POLL_MS);
  }
}

/** pickup 提交后、打开 delivery 前：锁定提货侧，防止 React 未 commit 即切换 */
export async function settlePickupBeforeDelivery(
  ctx: RPAContext,
  widgetSelector: string,
  pickup: AddressFields,
): Promise<void> {
  await blurAddressEditing(ctx, widgetSelector);
  await dismissAddressSuggestionDropdown(ctx.page);
  await forceDismissAddressSuggestionDropdown(ctx.page, widgetSelector);
  await ctx.page.keyboard.press("Tab").catch(() => undefined);
  await ctx.page.waitForTimeout(POLL_MS);

  const deadline = Date.now() + ADDRESS_SIDE_COMMIT_TIMEOUT_MS;
  while (Date.now() < deadline) {
    if (await isAddressPresentInWidget(ctx.page, widgetSelector, pickup)) {
      break;
    }
    await ctx.page.waitForTimeout(POLL_MS);
  }
  if (!(await isAddressPresentInWidget(ctx.page, widgetSelector, pickup))) {
    throw new RpaError(
      "ADDRESS_NOT_CONFIRMED",
      `提货地址未写入 widget：${pickup.mothershipDisplayLabel}`,
      { retryable: true },
    );
  }

  while (Date.now() < deadline) {
    if (await isPickupReadyForDeliveryInput(ctx.page, widgetSelector, pickup)) {
      break;
    }
    await releasePickupFieldForDelivery(ctx, widgetSelector, pickup);
    await ctx.page.waitForTimeout(POLL_MS);
  }

  const snapshot = await captureAddressCommitSnapshot(
    ctx,
    "pickup",
    widgetSelector,
  );
  logAddressCommitSnapshot("pickup-settled", snapshot);

  if (!(await isPickupReadyForDeliveryInput(ctx.page, widgetSelector, pickup))) {
    const diag = await captureAddressCommitSnapshot(ctx, "pickup", widgetSelector);
    console.log("[rpa] pickup-commit-diagnostic", {
      phase: "settlePickupBeforeDelivery",
      placeId: pickup.placeId,
      mothershipDisplayLabel: pickup.mothershipDisplayLabel,
      commitState: "widget_not_locked",
      placeRequestObserved: diag.axelLocationHit,
      inputValues: diag.inputValues,
      dropdownOpen: diag.dropdownOpen,
    });
    throw new RpaError(
      "ADDRESS_NOT_CONFIRMED",
      `提货地址未锁定，无法打开派送侧（${pickup.mothershipDisplayLabel}）`,
      { retryable: true },
    );
  }
}

/** 等待单侧地址被 MotherShip widget 接受（须 place/{id} 200，禁止仅以 input.value 判定） */
export async function waitForAddressSideCommit(
  ctx: RPAContext,
  side: "pickup" | "delivery",
  address: AddressFields,
  widgetSelector: string,
  streetInput?: Locator | null,
  placeCommit?: { bridge: AxelSelectionBridge; placeId: string },
): Promise<void> {
  const page = ctx.page;
  const placeId = placeCommit?.placeId?.trim();
  const bridge = placeCommit?.bridge;

  if (placeId && bridge) {
    if (bridge.hasPlaceCommit(placeId)) {
      const snapshot = await captureAddressCommitSnapshot(
        ctx,
        side,
        widgetSelector,
        true,
      );
      logAddressCommitSnapshot(`${side}-committed`, snapshot);
      return;
    }

    const committed = await bridge.waitForPlaceCommit(
      placeId,
      ADDRESS_SIDE_COMMIT_TIMEOUT_MS,
    );
    if (committed) {
      const snapshot = await captureAddressCommitSnapshot(
        ctx,
        side,
        widgetSelector,
        true,
      );
      logAddressCommitSnapshot(`${side}-committed`, snapshot);
      return;
    }

    const snapshot = await captureAddressCommitSnapshot(
      ctx,
      side,
      widgetSelector,
      false,
    );
    logAddressCommitSnapshot(`${side}-commit-failed`, snapshot);
    throw new RpaError(
      "ADDRESS_NOT_CONFIRMED",
      `${side === "pickup" ? "提货" : "派送"}地址 place/${placeId} 未 commit（网络证据缺失）`,
      { retryable: true },
    );
  }

  const queryHint = toQueryHint(address);
  const deadline = Date.now() + ADDRESS_SIDE_COMMIT_TIMEOUT_MS;
  let consecutiveSurfaceMatches = 0;

  while (Date.now() < deadline) {
    const dropdownOpen = await isAddressSuggestionDropdownOpen(page);
    if (dropdownOpen) {
      await dismissAddressSuggestionDropdown(page, streetInput ?? undefined);
    }

    const surface = await readCommittedInputSurface(page, widgetSelector);
    const surfaceOk = widgetContainsConfirmedAddress(surface, address);

    if (surfaceOk) {
      consecutiveSurfaceMatches += 1;
      if (consecutiveSurfaceMatches * POLL_MS >= ADDRESS_INPUT_STABLE_MS) {
        const snapshot = await captureAddressCommitSnapshot(
          ctx,
          side,
          widgetSelector,
          false,
        );
        logAddressCommitSnapshot(`${side}-committed`, snapshot);
        return;
      }
    } else {
      consecutiveSurfaceMatches = 0;
    }

    if (!surfaceOk && !dropdownOpen) {
      const widgetText = await readWidgetCombinedText(page, widgetSelector);
      if (widgetContainsConfirmedAddress(widgetText, address)) {
        const snapshot = await captureAddressCommitSnapshot(
          ctx,
          side,
          widgetSelector,
          false,
        );
        logAddressCommitSnapshot(`${side}-committed`, snapshot);
        return;
      }
    }

    if (streetInput) {
      const inputVisible = await streetInput.isVisible().catch(() => false);
      const inputEditable = await streetInput.isEditable().catch(() => false);
      if (inputVisible && inputEditable && (await isAddressInputCommitted(streetInput, queryHint))) {
        const snapshot = await captureAddressCommitSnapshot(
          ctx,
          side,
          widgetSelector,
          false,
        );
        logAddressCommitSnapshot(`${side}-committed`, snapshot);
        return;
      }
    }

    await page.waitForTimeout(POLL_MS);
  }

  const snapshot = await captureAddressCommitSnapshot(
    ctx,
    side,
    widgetSelector,
    false,
  );
  logAddressCommitSnapshot(`${side}-commit-failed`, snapshot);
  throw new RpaError(
    "ADDRESS_NOT_CONFIRMED",
    `${side === "pickup" ? "提货" : "派送"}地址未提交到 widget（${snapshot.widgetTextPreview.slice(0, 100)}）`,
    { retryable: true },
  );
}

/** 双地址快照校验；delivery 为 null 时仅校验 pickup 已锁定（非仅 input 文本回显） */
export async function assertDualAddressStable(
  ctx: RPAContext,
  widgetSelector: string,
  pickup: AddressFields,
  delivery: AddressFields | null,
): Promise<void> {
  const page = ctx.page;
  const deadline = Date.now() + ADDRESS_SIDE_COMMIT_TIMEOUT_MS;
  let lastPickupOk = false;
  let lastDeliveryOk = delivery === null;

  while (Date.now() < deadline) {
    if (delivery === null) {
      lastPickupOk = await isPickupReadyForDeliveryInput(
        page,
        widgetSelector,
        pickup,
      );
    } else {
      const surface = await readCommittedInputSurface(page, widgetSelector);
      const full = await readWidgetCombinedText(page, widgetSelector);
      const combined = `${surface} ${full}`;
      lastPickupOk =
        widgetContainsConfirmedAddress(surface, pickup) ||
        widgetContainsConfirmedAddress(full, pickup) ||
        widgetContainsConfirmedAddress(combined, pickup);
      lastDeliveryOk =
        widgetContainsConfirmedAddress(surface, delivery) ||
        widgetContainsConfirmedAddress(full, delivery) ||
        widgetContainsConfirmedAddress(combined, delivery);
    }

    if (lastPickupOk && lastDeliveryOk) {
      const snapshot = await captureAddressCommitSnapshot(
        ctx,
        "pickup",
        widgetSelector,
      );
      logAddressCommitSnapshot("dual-stable", {
        ...snapshot,
        side: "pickup",
        widgetTextPreview: snapshot.widgetTextPreview.slice(0, 220),
      });
      return;
    }

    if (delivery && lastPickupOk === false) {
      await blurAddressEditing(ctx, widgetSelector);
      const pickupSections = ctx.selectors.specs("RPA_SELECTOR_PICKUP_SECTION");
      if (pickupSections.length > 0) {
        await clickFirstVisibleFromSpecs(page, pickupSections).catch(
          () => undefined,
        );
      }
    }

    await page.waitForTimeout(POLL_MS);
  }

  const widgetText = await readWidgetCombinedText(page, widgetSelector);
  if (!lastPickupOk) {
    throw new RpaError(
      "ADDRESS_NOT_CONFIRMED",
      `双地址校验失败：提货地址已从 widget 消失（${pickup.mothershipDisplayLabel}）preview=${widgetText.slice(0, 120)}`,
      { retryable: true },
    );
  }
  if (delivery && !lastDeliveryOk) {
    throw new RpaError(
      "ADDRESS_NOT_CONFIRMED",
      `双地址校验失败：派送地址未锁定（${delivery.mothershipDisplayLabel}）preview=${widgetText.slice(0, 120)}`,
      { retryable: true },
    );
  }
}

export async function isAddressPresentInWidget(
  page: Page,
  widgetSelector: string,
  address: AddressFields,
): Promise<boolean> {
  const surface = await readCommittedInputSurface(page, widgetSelector);
  const full = await readWidgetCombinedText(page, widgetSelector);
  return widgetContainsConfirmedAddress(`${surface} ${full}`, address);
}

/** 纯函数：widget 文案是否已体现双地址（截图态：两州码 / 两门牌 / Freight details 旁双址） */
export function evaluateAddressesVisuallyComplete(widgetText: string): boolean {
  const normalized = widgetText.replace(/\s+/g, " ").trim();
  if (!normalized) {
    return false;
  }
  const stateCount = (normalized.match(/,\s*[A-Z]{2}\b/g) ?? []).length;
  const streetMarkers = normalized.match(/\d{3,5}\s+\S+/g) ?? [];
  const hasFreightChip = /freight details|货运详情/i.test(normalized);
  if (stateCount >= 2) {
    return true;
  }
  if (streetMarkers.length >= 2) {
    return true;
  }
  if (hasFreightChip && (stateCount >= 1 || streetMarkers.length >= 1)) {
    const hasDeliverTo = /deliver to|送货至/i.test(normalized);
    if (hasDeliverTo || stateCount >= 1) {
      return true;
    }
  }
  return false;
}

/**
 * 视觉优先：双地址已在 widget/地图体现即可进入货物步（不等 place 网络 / 下拉收起）。
 */
export async function isAddressesVisuallyCompleteForCargo(
  page: Page,
  widgetSelector: string,
  pickup?: AddressFields,
  delivery?: AddressFields,
): Promise<boolean> {
  const widgetText = await readWidgetCombinedText(page, widgetSelector).catch(
    () => "",
  );
  const surface = await readCommittedInputSurface(page, widgetSelector).catch(
    () => "",
  );
  const combined = `${surface} ${widgetText}`.replace(/\s+/g, " ").trim();

  if (pickup && delivery) {
    const pickupOk = widgetContainsConfirmedAddress(combined, pickup);
    const deliveryOk = widgetContainsConfirmedAddress(combined, delivery);
    if (pickupOk && deliveryOk) {
      return true;
    }
  }

  if (evaluateAddressesVisuallyComplete(combined)) {
    return true;
  }

  return page.evaluate(() => {
    const body = (document.body.innerText || "").replace(/\s+/g, " ");
    const stateHits = body.match(/,\s*[A-Z]{2}\b/g) ?? [];
    if (stateHits.length < 2) {
      return false;
    }
    const hasMap =
      document.querySelector("canvas") !== null ||
      document.querySelector("svg") !== null ||
      document.querySelector('[class*="map" i]') !== null;
    return hasMap;
  });
}

async function hasEmptyEditableStreetInput(
  page: Page,
  widgetSelector: string,
): Promise<boolean> {
  return page.evaluate(
    `(function(sel) {
      var widget = document.querySelector(sel);
      if (!widget) return false;
      var inputs = widget.querySelectorAll("input");
      for (var i = 0; i < inputs.length; i++) {
        var ph = (inputs[i].placeholder || "").toLowerCase();
        if (!/search|搜索/.test(ph)) continue;
        if (inputs[i].offsetParent === null) continue;
        var val = (inputs[i].value || "").replace(/\\s+/g, " ").trim();
        var editable =
          !inputs[i].disabled &&
          inputs[i].readOnly === false &&
          inputs[i].getAttribute("aria-readonly") !== "true";
        if (editable && val.length < 8) return true;
      }
      return false;
    })(${JSON.stringify(widgetSelector)})`,
  ) as Promise<boolean>;
}

/** 提货已写入 widget 且派送侧可独立输入（仅视觉 chip + Deliver to） */
export async function isPickupReadyForDeliveryInput(
  page: Page,
  widgetSelector: string,
  pickup: AddressFields,
): Promise<boolean> {
  await forceDismissAddressSuggestionDropdown(page, widgetSelector);
  return isPickupChipVisuallyReady(page, widgetSelector, pickup);
}

/** 提货点选后：鼠标点联想 + 视觉确认，再点「Deliver to」释放派送框 */
export async function lockPickupIntoChip(
  ctx: RPAContext,
  widgetSelector: string,
  streetInput?: Locator | null,
  pickup?: AddressFields,
): Promise<void> {
  const page = ctx.page;
  const widget = pageLocator(page, widgetSelector);

  if (
    pickup &&
    streetInput &&
    (await isAddressSuggestionDropdownOpen(page))
  ) {
    await mouseClickSuggestionUntilVisualConfirmed(
      page,
      streetInput,
      widgetSelector,
      pickup,
      "pickup-lock-chip",
      { blindFlow: isBlindAddressFlowActive(ctx) },
    );
    await page.waitForTimeout(300);
  }

  await dismissAddressSuggestionDropdown(page);

  const pickupLocked =
    pickup &&
    (await isAddressPresentInWidget(page, widgetSelector, pickup));

  if (!pickupLocked) {
    await page.keyboard.press("ArrowDown").catch(() => undefined);
    await page.waitForTimeout(100);
    if (streetInput) {
      await streetInput.press("Enter").catch(() => undefined);
      await page.waitForTimeout(250);
    }
    await page.keyboard.press("Tab").catch(() => undefined);
    await page.waitForTimeout(200);
  }
  await forceDismissAddressSuggestionDropdown(page, widgetSelector);

  const deliverTo = widget
    .getByText(/^(Deliver to|Delivery to|Where to\?)$/i)
    .first();
  if (await deliverTo.isVisible().catch(() => false)) {
    await deliverTo.click({ force: true, timeout: 3_000 }).catch(() => undefined);
  } else {
    await clickFirstVisibleFromSpecs(
      page,
      ctx.selectors.specs("RPA_SELECTOR_DELIVERY_SECTION"),
    ).catch(() => undefined);
  }
  await page.waitForTimeout(400);
  await dismissAddressSuggestionDropdown(page);
}

/** 点选提货后：收起下拉并打开派送侧，释放独立搜索框 */
export async function releasePickupFieldForDelivery(
  ctx: RPAContext,
  widgetSelector: string,
  pickup: AddressFields,
  streetInput?: Locator | null,
): Promise<boolean> {
  const page = ctx.page;

  if (await isPickupReadyForDeliveryInput(page, widgetSelector, pickup)) {
    return true;
  }

  const pickupInput =
    streetInput ?? (await visibleStreetInput(ctx, "pickup"));
  await lockPickupIntoChip(ctx, widgetSelector, pickupInput ?? undefined, pickup);
  return isPickupReadyForDeliveryInput(page, widgetSelector, pickup);
}

/** delivery 提交后若 pickup 被覆盖：交替 refill 直到双地址均出现在 widget */
export async function ensureDualAddressCommitted(
  ctx: RPAContext,
  widgetSelector: string,
  pickup: AddressFields,
  delivery: AddressFields,
  refillSide: (
    side: "pickup" | "delivery",
    address: AddressFields,
  ) => Promise<void>,
): Promise<void> {
  const page = ctx.page;
  const MAX_REFILL_ROUNDS = 6;

  for (let round = 0; round < MAX_REFILL_ROUNDS; round += 1) {
    await dismissAddressSuggestionDropdown(page);
    await blurAddressEditing(ctx, widgetSelector);

    const pickupOk = await isAddressPresentInWidget(
      page,
      widgetSelector,
      pickup,
    );
    const deliveryOk = await isAddressPresentInWidget(
      page,
      widgetSelector,
      delivery,
    );

    if (pickupOk && deliveryOk) {
      break;
    }

    if (!pickupOk) {
      console.log(
        `[rpa] address-commit:pickup-missing refill round=${round + 1}`,
      );
      await refillSide("pickup", pickup);
      continue;
    }
    if (!deliveryOk) {
      console.log(
        `[rpa] address-commit:delivery-missing refill round=${round + 1}`,
      );
      await refillSide("delivery", delivery);
      continue;
    }
  }

  await assertDualAddressStable(ctx, widgetSelector, pickup, delivery);
}

export async function waitForPickupCommit(
  ctx: RPAContext,
  pickup: AddressFields,
  widgetSelector: string,
  streetInput?: Locator | null,
): Promise<void> {
  await waitForAddressSideCommit(
    ctx,
    "pickup",
    pickup,
    widgetSelector,
    streetInput,
  );
}
