import { createHash } from "node:crypto";
import type { Locator, Page } from "playwright";
import {
  ADDRESS_CANDIDATE_SUGGESTION_TIMEOUT_MS,
  ADDRESS_SUGGESTION_TIMEOUT_MS,
  RPA_STREET_INPUT_CLICK_MS,
} from "@/lib/constants/rpa";
import { COLLECT_ADDRESS_LABELS_FROM_DOM, COLLECT_PAC_ITEM_LABELS, COLLECT_WIDGET_SUGGESTION_LABELS } from "@/workers/rpa/kernel/browser-scripts";
import { ensureArray } from "@/workers/rpa/kernel/context-validator";
import { pageLocator } from "@/lib/rpa/locator";
import { awaitHumanPacing } from "@/lib/rpa/human-pacing";
import { getSelector } from "@/lib/rpa/selectors";
import { getSelectorSpecs } from "@/lib/rpa/selector-specs";
import type {
  AddressLookupInput,
  MothershipAddressCandidate,
} from "@/modules/address/types";
import { resolveAddressSuggestionTimeoutMs } from "@/lib/rpa/env";
import { RpaError } from "@/modules/rpa/errors";
import type { QuoteRequest } from "@/modules/providers/quote-provider";
import { isPickupReadyForDeliveryInput } from "@/workers/rpa/address-commit";
import {
  formatPlaceCommitFailure,
  type AxelSelectionBridge,
  type AxelSelectionPayload,
} from "@/workers/rpa/axel-selection-bridge";
import { commitViaAxelPlacePrefetch } from "@/workers/rpa/axel-place-prefetch";
import {
  clickMothershipStyledSuggestion,
  waitForStyledSuggestionRows,
} from "@/workers/rpa/mothership-styled-suggestion-click";
import {
  captureSelectionTrace,
  logSelectionTrace,
} from "@/workers/rpa/selection-trace";
import type { RPAContext } from "@/workers/rpa/kernel/context";

const POLL_INTERVAL_MS = 250;
const MAX_SUGGESTION_ROW_LEN = 180;
const DEFAULT_MOTHERSHIP_COUNTRY = "USA";
const SUGGESTION_STABLE_POLL_MS = 150;

/** 输入框已有与查询等效文案时，避免 fill("") 造成界面「闪清」 */
export function normalizeAddressInputValue(value: string): string {
  return value.replace(/\s+/g, " ").trim();
}

export function addressInputMatchesQuery(current: string, query: string): boolean {
  const c = normalizeAddressInputValue(current);
  const q = normalizeAddressInputValue(query);
  if (!c || !q) {
    return false;
  }
  if (c === q) {
    return true;
  }
  const cLower = c.toLowerCase();
  const qLower = q.toLowerCase();
  if (cLower.includes(qLower) || qLower.includes(cLower)) {
    return true;
  }
  const streetNum = q.match(/^\d+/)?.[0];
  if (streetNum && cLower.startsWith(streetNum.toLowerCase()) && c.length >= 12) {
    return true;
  }
  return false;
}
const SUGGESTION_STABLE_PASSES = 2;

/** 候选枚举与 autocomplete 共用 query 序列，避免粘连-only 漏采真实地址 */
export function buildAddressEnumerationQueries(
  address: AddressLookupInput,
): string[] {
  return buildAddressAutocompleteQueries(address);
}

export function stripCountrySuffix(label: string): string {
  return label.replace(/,\s*USA\s*$/i, "").trim();
}
function toGluedStreetCity(street: string, city: string): string {
  const s = street.trim();
  const c = city.trim().replace(/\s+/g, " ");
  if (!s || !c) {
    return s;
  }
  if (s.toLowerCase().includes(c.toLowerCase())) {
    return s;
  }
  return `${s}${c}`;
}

function normalizeParsedStreet(
  street: string,
  query?: AddressLookupInput,
): string {
  const parsed = street.trim();
  const qStreet = query?.street?.trim();
  if (
    qStreet &&
    parsed.toLowerCase().startsWith(qStreet.toLowerCase()) &&
    parsed.length > qStreet.length
  ) {
    return qStreet;
  }
  return parsed;
}

function normalizeDomSuggestionLabel(label: string): string {
  return stripCountrySuffix(label).replace(/\s+/g, " ").trim();
}

function gluedVariants(label: string): string[] {
  const base = normalizeDomSuggestionLabel(label);
  const variants = new Set<string>([base]);
  const glued = base.replace(
    /\b(Street|St|Ave|Avenue|Blvd|Boulevard|Dr|Drive|Road|Rd|Lane|Ln)\s+([A-Z])/gi,
    "$1$2",
  );
  if (glued !== base) {
    variants.add(glued);
  }
  const spaced = base.replace(
    /\b(Street|St|Ave|Avenue|Blvd|Boulevard|Dr|Drive|Way|Road|Rd|Lane|Ln)\s*([A-Z])/gi,
    "$1 $2",
  );
  if (spaced !== base) {
    variants.add(spaced);
  }
  return [...variants];
}

async function locateSuggestionByLabelText(
  page: Page,
  text: string,
): Promise<Locator | null> {
  const normalized = text.replace(/\s+/g, " ").trim();
  const candidates = [
    normalized,
    stripCountrySuffix(normalized),
    normalized.split(",")[0]?.trim() ?? "",
    ...gluedVariants(normalized),
  ].filter((value, index, arr) => value.length >= 8 && arr.indexOf(value) === index);

  const roots = [
    page.locator(".pac-container .pac-item"),
    page.locator('[role="listbox"] [role="option"]'),
    getQuoteWidgetLocator(page).locator('[role="listbox"] [role="option"]'),
    page.locator("div[class*='sc-']"),
  ];

  for (const probe of candidates) {
    for (const root of roots) {
      const loc = root.filter({ hasText: probe }).first();
      if (await loc.isVisible().catch(() => false)) {
        return loc;
      }
    }

    const pageLoc = page.getByText(probe, { exact: false }).first();
    if (await pageLoc.isVisible().catch(() => false)) {
      return pageLoc;
    }

    const widgetLoc = getQuoteWidgetLocator(page)
      .getByText(probe, { exact: false })
      .first();
    if (await widgetLoc.isVisible().catch(() => false)) {
      return widgetLoc;
    }
  }

  return null;
}

function resolveCountry(address: AddressLookupInput): string {
  return address.country?.trim() || DEFAULT_MOTHERSHIP_COUNTRY;
}

/** MotherShip 输入框键入：仅街道+城市+州+国家（不含邮编，对齐 PRD GD-15） */
export function formatAddressQuery(address: AddressLookupInput): string {
  const street = address.street.trim();
  const city = address.city.trim();
  const state = address.state.trim();
  return `${street}, ${city}, ${state}, ${resolveCountry(address)}`;
}

const STREET_SUFFIX_PATTERN =
  "Street|St|Road|Rd|Avenue|Ave|Boulevard|Blvd|Drive|Dr|Lane|Ln|Way|Court|Ct|Circle|Cir|Place|Pl|Terrace|Ter|Highway|Hwy|Parkway|Pkwy|Freeway|Fwy";

/**
 * 从 mothership label / 粘连文案提取纯街道名。
 * 取最后一个街道后缀（Street/Ave/Blvd…），再回溯门牌号与街名，避免 POI 前缀污染查询。
 */
export function extractStreetFromLabel(label: string): string | null {
  const normalized = label.replace(/\s+/g, " ").trim();
  if (!normalized) {
    return null;
  }

  const suffixRe = new RegExp(
    `\\b(${STREET_SUFFIX_PATTERN})(?=\\s*,|\\s|$|[A-Z][a-z])`,
    "gi",
  );
  let lastSuffix: RegExpExecArray | null = null;
  let match: RegExpExecArray | null = suffixRe.exec(normalized);
  while (match) {
    const token = match[1];
    const idx = match.index;
    if (token.toLowerCase() === "st") {
      const prevChar = idx > 0 ? normalized[idx - 1] : "";
      if (prevChar && /[a-z]/i.test(prevChar)) {
        match = suffixRe.exec(normalized);
        continue;
      }
      const head = normalized.slice(0, idx).trimEnd();
      if (!/\s\S+$/.test(head)) {
        match = suffixRe.exec(normalized);
        continue;
      }
    }
    lastSuffix = match;
    match = suffixRe.exec(normalized);
  }
  if (!lastSuffix) {
    return null;
  }

  const suffix = lastSuffix[1];
  const before = normalized.slice(0, lastSuffix.index).replace(/,\s*$/, "").trimEnd();
  const commaIdx = before.lastIndexOf(",");
  const segment = (commaIdx >= 0 ? before.slice(commaIdx + 1) : before).trim();
  if (!segment) {
    return null;
  }

  const numbered = segment.match(/^(\d+)\s+(.+)$/);
  const number = numbered?.[1];
  const namePart = (numbered?.[2] ?? segment).trim();
  const words = namePart.split(/\s+/).filter(Boolean);
  if (!number && words.length === 0) {
    return null;
  }

  const streetName = number ? `${number} ${words.join(" ")}`.trim() : words[words.length - 1];
  const street = `${streetName} ${suffix}`.replace(/\s+/g, " ").trim();
  if (street.length < 8 || street.length > 80) {
    return null;
  }
  return street;
}

/** 将 POI 粘连 street 归一为纯街道，供联想键入与 DOM 采集使用 */
export function resolveAddressAutocompleteInput(
  address: AddressLookupInput,
  options?: { mothershipLabel?: string },
): AddressLookupInput {
  const label = options?.mothershipLabel?.trim();
  const sources = [label, address.street.trim()].filter(Boolean) as string[];

  for (const source of sources) {
    const pureStreet = extractStreetFromLabel(source);
    if (!pureStreet) {
      continue;
    }
    const current = address.street.trim();
    if (pureStreet.toLowerCase() === current.toLowerCase()) {
      return address;
    }
    return { ...address, street: pureStreet };
  }

  return address;
}

/** 联想未出现时依次尝试的键入文案（纯街道优先，完整 POI 置后） */
export function buildAddressAutocompleteQueries(
  address: AddressLookupInput,
  options?: { mothershipLabel?: string; preferConfirmedLabel?: boolean },
): string[] {
  const resolved = resolveAddressAutocompleteInput(address, options);
  const label = options?.mothershipLabel?.trim();
  const street = resolved.street.trim();
  const city = resolved.city.trim();
  const state = resolved.state.trim();
  const origStreet = address.street.trim();

  const queries: string[] = [];
  if (options?.preferConfirmedLabel && label) {
    queries.push(stripCountrySuffix(label));
  }
  queries.push(formatAddressQuery(resolved));
  queries.push(`${street}, ${city}, ${state}`);
  queries.push(`${toGluedStreetCity(street, city)}, ${state}`);

  const streetToken = street.match(/^\d+\s+\S+(?:\s+\S+)?/)?.[0]?.trim();
  if (streetToken && streetToken.length >= 6) {
    queries.push(streetToken);
  }

  if (origStreet && origStreet.toLowerCase() !== street.toLowerCase()) {
    queries.push(formatAddressQuery(address));
    queries.push(`${origStreet}, ${city}, ${state}`);
    queries.push(`${toGluedStreetCity(origStreet, city)}, ${state}`);
    if (label) {
      queries.push(stripCountrySuffix(label));
    }
  } else if (label) {
    const stripped = stripCountrySuffix(label);
    if (stripped !== queries[0]) {
      queries.push(stripped);
    }
  }

  return [...new Set(queries.map((q) => q.trim()).filter(Boolean))];
}

export function formatMothershipFormattedAddress(
  street: string,
  city: string,
  state: string,
  country = DEFAULT_MOTHERSHIP_COUNTRY,
): string {
  return `${street}, ${city}, ${state}, ${country}`;
}

function queryStreetNumber(address: AddressLookupInput): string | null {
  return address.street.match(/^\d+/)?.[0] ?? null;
}

function labelsContainStreetNumber(labels: readonly string[], streetNum: string): boolean {
  return labels.some((label) => label.includes(streetNum));
}

/** place 绑定优先尝试的 label 序列（完整街道优先于粘连 display_label） */
export function buildAddressBindingLabels(
  address: AddressLookupInput,
  mothershipLabel?: string,
): string[] {
  const street = address.street.trim();
  const city = address.city.trim();
  const state = address.state.trim();
  const labels: string[] = [];
  if (street && city && state) {
    labels.push(`${street}, ${city}, ${state}`);
    labels.push(formatAddressQuery(address));
  }
  const glued = mothershipLabel?.trim();
  if (glued) {
    labels.push(stripCountrySuffix(glued));
  }
  return [...new Set(labels.map((l) => l.trim()).filter(Boolean))];
}

function enrichCandidateFromQuery(
  item: MothershipAddressCandidate,
  query?: AddressLookupInput,
): MothershipAddressCandidate {
  if (!query) {
    return item;
  }
  const country = resolveCountry(query);
  const zip = item.zip || query.zip;

  let street = item.street.trim();
  const queryStreetNum = queryStreetNumber(query);
  const itemStreetNum = street.match(/^\d+/)?.[0];
  if (queryStreetNum && !itemStreetNum) {
    const nameFromItem = street.replace(/\s+/g, " ").trim();
    const nameFromQuery = query.street.replace(/^\d+\s*/, "").trim();
    const streetName = nameFromItem.length >= 3 ? nameFromItem : nameFromQuery;
    street = `${queryStreetNum} ${streetName}`.trim();
  }

  const city = item.city.trim() || query.city.trim();
  const state = item.state.trim() || query.state.trim();
  const hasStructured = Boolean(city) && Boolean(state) && Boolean(street);
  const formatted_address = hasStructured
    ? formatMothershipFormattedAddress(street, city, state, country)
    : item.formatted_address;
  const display_label =
    queryStreetNum && !itemStreetNum && hasStructured
      ? `${toGluedStreetCity(street, city)}, ${state}`
      : item.display_label;

  return {
    ...item,
    street,
    city,
    state,
    zip,
    formatted_address,
    display_label,
  };
}

function toOptionId(label: string): string {
  return `ms_${createHash("sha256").update(label).digest("hex").slice(0, 16)}`;
}

const US_STATE_NAME_TO_CODE: Record<string, string> = {
  alabama: "AL",
  alaska: "AK",
  arizona: "AZ",
  arkansas: "AR",
  california: "CA",
  colorado: "CO",
  connecticut: "CT",
  delaware: "DE",
  florida: "FL",
  georgia: "GA",
  texas: "TX",
  // 其余州按需扩展；MotherShip 常见 CA/TX
};

function resolveStateCode(raw: string, query?: AddressLookupInput): string {
  const trimmed = raw.trim();
  if (/^[A-Z]{2}$/.test(trimmed)) {
    return trimmed;
  }
  const mapped = US_STATE_NAME_TO_CODE[trimmed.toLowerCase()];
  if (mapped) {
    return mapped;
  }
  return query?.state?.trim() || trimmed.slice(0, 2).toUpperCase();
}

/** 解析 pac-item 结构化标签 main|secondary 或粘连 innerText */
function parsePacStructuredLabel(
  label: string,
  query?: AddressLookupInput,
): MothershipAddressCandidate | null {
  const pipeIdx = label.indexOf("|");
  if (pipeIdx > 0) {
    const street = label.slice(0, pipeIdx).trim();
    const secondary = stripCountrySuffix(label.slice(pipeIdx + 1).trim());
    const parts = secondary.split(",").map((p) => p.trim()).filter(Boolean);
    if (street.length >= 5 && parts.length >= 2) {
      const city = parts[0];
      const stateToken =
        [...parts].reverse().find((part) => /^[A-Z]{2}$/.test(part)) ??
        parts.find((part) => US_STATE_NAME_TO_CODE[part.toLowerCase()]);
      const state = resolveStateCode(stateToken ?? parts[parts.length - 1], query);
      const item: MothershipAddressCandidate = {
        option_id: toOptionId(label),
        display_label: `${toGluedStreetCity(street, city)}, ${state}`,
        formatted_address: formatMothershipFormattedAddress(street, city, state),
        street,
        city,
        state,
        zip: query?.zip ?? "",
      };
      return enrichCandidateFromQuery(item, query);
    }
  }

  const normalized = stripCountrySuffix(label.replace(/\s+/g, " ").trim());
  
  // 含 suburb 格式：street city, suburb, state
  const suburbMatch = normalized.match(
    /^(\d+\s+(?:[\w.]+\s+)*?(?:Street|St|Blvd|Boulevard|Dr|Drive|Way|Road|Rd|Lane|Ln|Ave|Avenue)\.?)\s+([A-Z][A-Za-z\s.]+?),\s*([^,]+),\s*([A-Za-z]{2,})$/,
  );
  if (suburbMatch) {
    const [, street, city, , stateRaw] = suburbMatch;
    const state = resolveStateCode(stateRaw, query);
    const item: MothershipAddressCandidate = {
      option_id: toOptionId(label),
      display_label: `${toGluedStreetCity(street.trim(), city.trim())}, ${state}`,
      formatted_address: formatMothershipFormattedAddress(
        street.trim(),
        city.trim(),
        state,
      ),
      street: street.trim(),
      city: city.trim(),
      state,
      zip: query?.zip ?? "",
    };
    return enrichCandidateFromQuery(item, query);
  }

  // 无门牌号街道（Fifth Avenue / Hollywood Boulevard 等）
  const namedStreetMatch = normalized.match(
    /^([A-Z][A-Za-z0-9\s.'-]*(?:Avenue|Ave|Boulevard|Blvd|Street|St|Road|Rd|Drive|Dr|Way|Lane|Ln|Place|Pl|Court|Ct|Highway|Hwy)(?:\s+(?:North|South|East|West|N|S|E|W|NE|NW|SE|SW))?)\s+([A-Z][A-Za-z\s.]+?),\s*([A-Z]{2})$/,
  );
  if (namedStreetMatch) {
    const [, street, city, state] = namedStreetMatch;
    const item: MothershipAddressCandidate = {
      option_id: toOptionId(label),
      display_label: `${toGluedStreetCity(street.trim(), city.trim())}, ${state}`,
      formatted_address: formatMothershipFormattedAddress(
        street.trim(),
        city.trim(),
        state,
      ),
      street: street.trim(),
      city: city.trim(),
      state,
      zip: query?.zip ?? "",
    };
    return enrichCandidateFromQuery(item, query);
  }

  // 无 suburb 格式 (styled-components)：street city, state
  // 例如："1234 Warehouse Street Los Angeles, CA"
  const directCityMatch = normalized.match(
    /^(\d+\s+(?:[\w.]+\s+)*?(?:Street|St|Blvd|Boulevard|Dr|Drive|Way|Road|Rd|Lane|Ln|Ave|Avenue)\.?)\s+([A-Z][A-Za-z\s.]+?),\s*([A-Z]{2})$/,
  );
  if (directCityMatch) {
    const [, street, city, state] = directCityMatch;
    const item: MothershipAddressCandidate = {
      option_id: toOptionId(label),
      display_label: `${toGluedStreetCity(street.trim(), city.trim())}, ${state}`,
      formatted_address: formatMothershipFormattedAddress(
        street.trim(),
        city.trim(),
        state,
      ),
      street: street.trim(),
      city: city.trim(),
      state,
      zip: query?.zip ?? "",
    };
    return enrichCandidateFromQuery(item, query);
  }

  return null;
}

/** 解析 MotherShip 联想文案（含 StreetLos Angeles 粘连格式） */
export function labelToCandidate(
  label: string,
  query?: AddressLookupInput,
): MothershipAddressCandidate | null {
  const trimmed = label.replace(/\s+/g, " ").trim();
  if (trimmed.length < 5) {
    return null;
  }

  const pacParsed = parsePacStructuredLabel(trimmed, query);
  if (pacParsed) {
    return pacParsed;
  }

  const standard = trimmed.match(
    /^(.+?),\s*([^,]+),\s*([A-Z]{2})\s*(\d{5}(?:-\d{4})?)/,
  );
  if (standard) {
    const [, street, city, state, zip] = standard;
    const item: MothershipAddressCandidate = {
      option_id: toOptionId(trimmed),
      display_label: trimmed.replace(/\s+\d{5}(-\d{4})?$/, "").trim(),
      formatted_address: formatMothershipFormattedAddress(
        street.trim(),
        city.trim(),
        state,
      ),
      street: street.trim(),
      city: city.trim(),
      state,
      zip,
    };
    return enrichCandidateFromQuery(item, query);
  }

  const tail = trimmed.match(/,\s*([A-Z]{2})\s+(\d{5}(?:-\d{4})?)$/);
  if (tail) {
    const state = tail[1];
    const zip = tail[2];
    const beforeState = trimmed
      .slice(0, trimmed.lastIndexOf(tail[0]))
      .trim()
      .replace(/,\s*$/, "");
    const glued = beforeState.match(/^(.+?[a-z])([A-Z][\sA-Za-z.']+)$/);
    if (glued) {
      const street = glued[1].trim();
      const city = glued[2].trim();
      const item: MothershipAddressCandidate = {
        option_id: toOptionId(trimmed),
        display_label: `${street}${city}, ${state}`.replace(
          /\s+\d{5}(-\d{4})?$/,
          "",
        ),
        formatted_address: formatMothershipFormattedAddress(street, city, state),
        street,
        city,
        state,
        zip,
      };
      return enrichCandidateFromQuery(item, query);
    }
    if (query && query.zip === zip) {
      const item: MothershipAddressCandidate = {
        option_id: toOptionId(trimmed),
        display_label: trimmed.replace(/\s+\d{5}(-\d{4})?$/, "").trim(),
        formatted_address: formatMothershipFormattedAddress(
          query.street,
          query.city,
          query.state,
          resolveCountry(query),
        ),
        street: query.street,
        city: query.city,
        state: query.state,
        zip,
      };
      return enrichCandidateFromQuery(item, query);
    }
  }

  // 去掉 ", USA" / ", US" 等国家后缀后再匹配
  const trimmedNoCountry = stripCountrySuffix(trimmed);
  const stateOnly = trimmedNoCountry.match(/,\s*([A-Z]{2})\s*$/);
  if (stateOnly && query && query.state === stateOnly[1]) {
    const state = stateOnly[1];
    const beforeState = trimmedNoCountry
      .slice(0, trimmedNoCountry.lastIndexOf(stateOnly[0]))
      .trim();
    const glued = beforeState.match(/^(.+?[a-z])([A-Z][\sA-Za-z.']+)$/);
    if (glued) {
      const street = glued[1].trim();
      const city = glued[2].trim();
      const item: MothershipAddressCandidate = {
        option_id: toOptionId(trimmed),
        display_label: trimmed,
        formatted_address: formatMothershipFormattedAddress(street, city, state),
        street,
        city,
        state,
        zip: "",
      };
      return enrichCandidateFromQuery(item, query);
    }
    // 标准逗号分隔格式（无 ZIP）：street, city, state
    const commaSpaced = beforeState.match(/^(.+?),\s*([^,]+)$/);
    if (commaSpaced) {
      const street = commaSpaced[1].trim();
      const city = commaSpaced[2].trim();
      const item: MothershipAddressCandidate = {
        option_id: toOptionId(trimmed),
        display_label: `${toGluedStreetCity(street, city)}, ${state}`,
        formatted_address: formatMothershipFormattedAddress(street, city, state),
        street,
        city,
        state,
        zip: query?.zip ?? "",
      };
      return enrichCandidateFromQuery(item, query);
    }
  }

  if (query && isLikelySuggestionRow(trimmed, query)) {
    const normalized = normalizeDomSuggestionLabel(trimmed);
    const spaced = normalized.match(/^(.+?),\s*([^,]+),\s*([A-Z]{2})$/);
    if (spaced) {
      const street = normalizeParsedStreet(spaced[1], query);
      const city = spaced[2].trim();
      const state = spaced[3];
      const item: MothershipAddressCandidate = {
        option_id: toOptionId(trimmed),
        display_label: `${toGluedStreetCity(street, city)}, ${state}`,
        formatted_address: formatMothershipFormattedAddress(
          street,
          city,
          state,
          resolveCountry(query),
        ),
        street,
        city,
        state,
        zip: query.zip,
      };
      return enrichCandidateFromQuery(item, query);
    }
  }

  const streetOnly = parseStreetOnlySuggestion(trimmed, query);
  if (streetOnly) {
    return streetOnly;
  }

  return null;
}

/** MotherShip listbox 仅返回街道行（无城市/州）时的候选补全 */
function parseStreetOnlySuggestion(
  label: string,
  query?: AddressLookupInput,
): MothershipAddressCandidate | null {
  if (!query) {
    return null;
  }
  const trimmed = label.replace(/\s+/g, " ").trim();
  if (trimmed.includes(",") || trimmed.includes("|")) {
    return null;
  }
  const streetNum = query.street.match(/^\d+/)?.[0];
  if (!streetNum || !trimmed.startsWith(streetNum)) {
    return null;
  }
  if (trimmed.length < 8 || trimmed.length > MAX_SUGGESTION_ROW_LEN) {
    return null;
  }
  if (
    !/\b(street|st|avenue|ave|blvd|boulevard|dr|drive|road|rd|lane|ln|way)\b/i.test(
      trimmed,
    )
  ) {
    return null;
  }
  if (isInputEcho(trimmed, query)) {
    return null;
  }
  const street = trimmed;
  const city = query.city.trim();
  const state = query.state.trim();
  const item: MothershipAddressCandidate = {
    option_id: toOptionId(`${trimmed}|${city}|${state}`),
    display_label: `${toGluedStreetCity(street, city)}, ${state}`,
    formatted_address: formatMothershipFormattedAddress(
      street,
      city,
      state,
      resolveCountry(query),
    ),
    street,
    city,
    state,
    zip: query.zip,
  };
  return enrichCandidateFromQuery(item, query);
}

/** 候选是否为「用户输入回显」而非 MotherShip 独立坐标 */
export function isMotherShipEchoCandidate(
  item: MothershipAddressCandidate,
  query: AddressLookupInput,
): boolean {
  const norm = (s: string) => s.replace(/\s+/g, " ").trim().toLowerCase();
  if (norm(item.street) !== norm(query.street)) {
    return false;
  }
  if (norm(item.city) !== norm(query.city)) {
    return false;
  }
  const glued = `${toGluedStreetCity(query.street, query.city)}, ${query.state}`;
  const compact = (s: string) => s.replace(/\s+/g, "").toLowerCase();
  return (
    compact(item.display_label) === compact(glued) ||
    compact(item.display_label) ===
      compact(`${query.street}, ${query.city}, ${query.state}`)
  );
}

function getQuoteWidgetLocator(page: Page): Locator {
  return pageLocator(page, getSelector("RPA_SELECTOR_QUOTE_WIDGET")).first();
}

function isInputEcho(text: string, query: AddressLookupInput): boolean {
  const normalized = text.replace(/\s+/g, " ").trim();
  for (const candidate of buildAddressAutocompleteQueries(query)) {
    if (normalized === candidate || normalized === stripCountrySuffix(candidate)) {
      return true;
    }
  }
  return (
    normalized === `${query.street}, ${query.city}, ${query.state}` ||
    normalized ===
      `${query.street}, ${query.city}, ${query.state}, ${resolveCountry(query)}`
  );
}

function isMotherShipEnumerationRow(
  text: string,
  query: AddressLookupInput,
): boolean {
  if (/use my location|使用我的位置/i.test(text)) {
    return false;
  }
  if (text.includes("|")) {
    const main = text.split("|")[0]?.trim() ?? "";
    const secondary = text.slice(text.indexOf("|") + 1).trim();
    if (main.length < 5 || secondary.length < 2) {
      return false;
    }
    const compact = (s: string) =>
      s.replace(/\s+/g, "").toLowerCase().replace(/,usa$/i, "");
    const echoSecondary = compact(`${query.city}, ${query.state}`);
    if (compact(secondary).startsWith(echoSecondary)) {
      return false;
    }
    return true;
  }
  const probe = text;
  if (isInputEcho(probe, query) || isInputEcho(text, query)) {
    return false;
  }
  const normalized = text.replace(/\s+/g, " ").trim();
  if (normalized.length < 8) {
    return false;
  }
  const streetNum = query.street.match(/^\d+/)?.[0];
  if (streetNum && normalized.includes(streetNum)) {
    return true;
  }
  const cityToken = query.city.trim().split(/\s+/)[0]?.toLowerCase();
  return Boolean(
    cityToken &&
      cityToken.length >= 3 &&
      normalized.toLowerCase().includes(cityToken),
  );
}

/** @internal 单测：listbox pipe 行过滤 */
export function isMotherShipEnumerationRowForTest(
  text: string,
  query: AddressLookupInput,
): boolean {
  return isMotherShipEnumerationRow(text, query);
}

function isLikelySuggestionRow(
  text: string,
  query: AddressLookupInput,
): boolean {
  const normalized = text.replace(/\s+/g, " ").trim();
  if (normalized.length < 8 || normalized.length > MAX_SUGGESTION_ROW_LEN) {
    return false;
  }
  if (isInputEcho(normalized, query)) {
    return false;
  }
  const streetNum = query.street.match(/^\d+/)?.[0];
  if (streetNum) {
    if (!normalized.includes(streetNum)) {
      return false;
    }
  } else {
    const streetTokens = query.street
      .trim()
      .toLowerCase()
      .split(/\s+/)
      .filter((token) => token.length >= 4);
    if (
      streetTokens.length > 0 &&
      !streetTokens.some((token) => normalized.toLowerCase().includes(token))
    ) {
      return false;
    }
  }
  if (query.state && normalized.includes(query.state)) {
    return true;
  }
  const cityToken = query.city.trim().split(/\s+/)[0];
  if (
    cityToken.length >= 3 &&
    normalized.toLowerCase().includes(cityToken.toLowerCase())
  ) {
    return true;
  }
  const streetWord = query.street.split(/\s+/)[1];
  return Boolean(
    streetWord &&
      streetWord.length >= 3 &&
      normalized.toLowerCase().includes(streetWord.toLowerCase()),
  );
}

function streetSearchTokens(query: AddressLookupInput): string[] {
  const streetNum = query.street.match(/^\d+/)?.[0] ?? "";
  const streetWords = query.street.split(/\s+/).slice(1, 3).join(" ").trim();
  const tokens = [
    streetNum && streetWords ? `${streetNum} ${streetWords}` : "",
    streetNum,
  ].filter((token) => token.length >= 4);
  return [...new Set(tokens)];
}

function normalizeListboxLabel(text: string): string {
  const lines = text
    .split(/\r?\n/)
    .map((line) => line.trim())
    .filter(Boolean);
  if (lines.length >= 2 && !/use my location|使用我的位置/i.test(lines.join(" "))) {
    return `${lines[0]}|${lines.slice(1).join(", ")}`;
  }
  return text.replace(/\s+/g, " ").trim();
}

async function collectSuggestionLabelsFromWidget(
  page: Page,
  query: AddressLookupInput,
): Promise<string[]> {
  const rawLabels: string[] = ensureArray(
    await page
      .evaluate(COLLECT_WIDGET_SUGGESTION_LABELS)
      .catch(() => []) as string[] | null | undefined,
  ) as string[];
  // 不过滤：COLLECT_WIDGET_SUGGESTION_LABELS 已内置 skipRe 过滤
  // 对于 styled-components 检测，过严的 isMotherShipEnumerationRow 会漏掉合法联想
  if (rawLabels.length > 0) {
    return rawLabels;
  }
  // 回退：使用严格过滤
  return rawLabels.filter((label) => isMotherShipEnumerationRow(label, query));
}

async function collectEnumerationLabels(
  page: Page,
  query: AddressLookupInput,
): Promise<string[]> {
  const widgetLabels = await collectSuggestionLabelsFromWidget(page, query);
  if (widgetLabels.length > 0) {
    return widgetLabels;
  }
  const merged = await collectAddressSuggestionLabels(page, query);
  const filtered = merged.filter((label) =>
    isMotherShipEnumerationRow(label, query),
  );
  // 为何：waitFor 已确认 DOM 有联想，过严 filter 会把 pac-item 真实候选全部滤掉
  return filtered.length > 0 ? filtered : merged;
}

async function collectSuggestionLabelsFromPacStructured(
  page: Page,
  query: AddressLookupInput,
): Promise<string[]> {
  const rawLabels: string[] = ensureArray(
    await page
      .evaluate(COLLECT_PAC_ITEM_LABELS)
      .catch(() => []) as string[] | null | undefined,
  ) as string[];
  return rawLabels.filter((label: string) => {
    const main = label.includes("|") ? label.split("|")[0] : label;
    return isLikelySuggestionRow(main, query) || isLikelySuggestionRow(label, query);
  });
}

async function collectSuggestionLabelsFromPacContainer(
  page: Page,
  query: AddressLookupInput,
): Promise<string[]> {
  const labels: string[] = [];
  const roots = [
    page.locator(".pac-container"),
    page.locator('[role="listbox"]'),
    page.locator('[class*="suggestion" i], [class*="Suggestion"]'),
  ];

  for (const root of roots) {
    const rootCount = await root.count().catch(() => 0);
    for (let r = 0; r < rootCount; r += 1) {
      const container = root.nth(r);
      if (!(await container.isVisible().catch(() => false))) {
        continue;
      }
      let itemLocator = container.locator(".pac-item, [role='option']");
      let count = await itemLocator.count().catch(() => 0);
      if (count === 0) {
        itemLocator = container.locator("li, button");
        count = await itemLocator.count().catch(() => 0);
      }
      for (let i = 0; i < count; i += 1) {
        const text = await itemLocator.nth(i).innerText().catch(() => "");
        const normalized = text.replace(/\s+/g, " ").trim();
        if (isLikelySuggestionRow(normalized, query)) {
          labels.push(normalized);
        }
      }
    }
  }

  return labels;
}

/** 对齐录制：page.getByText(门牌号+街道) 点选，下拉项常不含 zip */
async function collectSuggestionLabelsByTextMatch(
  page: Page,
  query: AddressLookupInput,
): Promise<string[]> {
  const labels: string[] = [];

  for (const token of streetSearchTokens(query)) {
    const loc = page.getByText(token, { exact: false });
    const count = await loc.count().catch(() => 0);
    for (let i = 0; i < Math.min(count, 12); i += 1) {
      const el = loc.nth(i);
      if (!(await el.isVisible().catch(() => false))) {
        continue;
      }
      const text = await el.innerText().catch(() => "");
      const normalized = text.replace(/\s+/g, " ").trim();
      if (isLikelySuggestionRow(normalized, query)) {
        labels.push(normalized);
      }
    }
  }

  return labels;
}

async function collectSuggestionLabelsFromSpecs(page: Page): Promise<string[]> {
  const specs = getSelectorSpecs("RPA_SELECTOR_ADDRESS_SUGGESTION");
  const labels: string[] = [];
  const seen = new Set<string>();

  for (const spec of specs) {
    const root = pageLocator(page, spec);
    const count = await root.count().catch(() => 0);
    for (let i = 0; i < count; i++) {
      const text = await root
        .nth(i)
        .innerText()
        .catch(() => "");
      const normalized = text.replace(/\s+/g, " ").trim();
      if (normalized && !seen.has(normalized)) {
        seen.add(normalized);
        labels.push(normalizeListboxLabel(text));
      }
    }
  }

  return labels;
}

/** MotherShip 联想：全页扫描（含 body 级 .pac-container portal） */
async function collectSuggestionLabelsByHeuristic(
  page: Page,
  query: AddressLookupInput,
): Promise<string[]> {
  const streetNum = query.street.match(/^\d+/)?.[0] ?? "";
  const cityToken = query.city.trim().split(/\s+/)[0] ?? "";

  const rawLabels: string[] = ensureArray(
    await page
      .evaluate(COLLECT_ADDRESS_LABELS_FROM_DOM, {
        streetNumber: streetNum,
        cityHint: cityToken,
      })
      .catch(() => []) as string[] | null | undefined,
  ) as string[];

  return rawLabels.filter((label: string) => isLikelySuggestionRow(label, query));
}

export async function collectAddressSuggestionLabels(
  page: Page,
  query: AddressLookupInput,
): Promise<string[]> {
  const seen = new Set<string>();
  const merged: string[] = [];

  for (const label of [
    // 优先：COLLECT_WIDGET_SUGGESTION_LABELS（含 styled-components 检测）
    ...(await collectSuggestionLabelsFromWidget(page, query)),
    ...(await collectSuggestionLabelsFromPacStructured(page, query)),
    ...(await collectSuggestionLabelsFromPacContainer(page, query)),
    ...(await collectSuggestionLabelsFromSpecs(page)),
    ...(await collectSuggestionLabelsByTextMatch(page, query)),
    ...(await collectSuggestionLabelsByHeuristic(page, query)),
  ]) {
    const normalized = label.replace(/\s+/g, " ").trim();
    if (normalized && !seen.has(normalized)) {
      seen.add(normalized);
      merged.push(normalized);
    }
  }

  return merged;
}

/** 等待下拉项数量稳定后再采集（避免首条即返回漏掉其余选项） */
export async function collectAddressSuggestionLabelsWhenStable(
  page: Page,
  query: AddressLookupInput,
  options?: { timeoutMs?: number },
): Promise<string[]> {
  const timeoutMs =
    options?.timeoutMs ?? ADDRESS_CANDIDATE_SUGGESTION_TIMEOUT_MS;
  const deadline = Date.now() + timeoutMs;
  let best: string[] = [];
  let stablePasses = 0;

  while (Date.now() < deadline) {
    const labels = await collectAddressSuggestionLabels(page, query);
    if (labels.length >= 2) {
      return labels;
    }
    if (labels.length > best.length) {
      best = labels;
      stablePasses = 0;
    } else if (labels.length === best.length && best.length > 0) {
      stablePasses += 1;
      if (stablePasses >= SUGGESTION_STABLE_PASSES) {
        return best;
      }
    }
    await page.waitForTimeout(SUGGESTION_STABLE_POLL_MS);
  }

  return best;
}

/** 候选 API：多 query 尝试 + WhenStable 采集（不依赖 waitFor 误报） */
export async function enumerateAddressCandidates(
  page: Page,
  input: Locator,
  address: AddressLookupInput,
): Promise<string[]> {
  const queries = buildAddressEnumerationQueries(address);
  const seen = new Set<string>();
  const merged: string[] = [];
  let lastQuery = queries[0] ?? formatAddressQuery(address);
  const waitMs = ADDRESS_CANDIDATE_SUGGESTION_TIMEOUT_MS;

  const absorb = (labels: string[]) => {
    for (const label of labels) {
      const normalized = label.replace(/\s+/g, " ").trim();
      if (normalized && !seen.has(normalized)) {
        seen.add(normalized);
        merged.push(normalized);
      }
    }
  };

  for (let i = 0; i < queries.length; i += 1) {
    const queryText = queries[i];
    lastQuery = queryText;

    await triggerAddressAutocomplete(input, queryText, page, {
      suggestionTimeoutMs: waitMs,
      forEnumeration: true,
    });

    absorb(
      await collectAddressSuggestionLabelsWhenStable(page, address, {
        timeoutMs: waitMs,
      }),
    );

    const streetNum = queryStreetNumber(address);
    if (
      merged.length > 0 &&
      (streetNum == null || labelsContainStreetNumber(merged, streetNum))
    ) {
      break;
    }
  }

  if (merged.length === 0) {
    throw new RpaError(
      "ADDRESS_SUGGESTION_NOT_FOUND",
      `MotherShip 地址联想未出现：${lastQuery}`,
      { retryable: false },
    );
  }

  return merged;
}

export async function triggerAddressAutocomplete(
  input: Locator,
  query: string,
  page?: Page,
  options?: { suggestionTimeoutMs?: number; forEnumeration?: boolean },
): Promise<void> {
  const suggestionTimeoutMs =
    options?.suggestionTimeoutMs ?? ADDRESS_SUGGESTION_TIMEOUT_MS;
  const forEnumeration = options?.forEnumeration ?? false;

  await input.scrollIntoViewIfNeeded().catch(() => undefined);

  const focused = await input
    .focus({ timeout: RPA_STREET_INPUT_CLICK_MS })
    .then(() => true)
    .catch(() => false);

  if (!focused) {
    await input.click({ timeout: 3_000, force: true }).catch(() => undefined);
  }

  const currentValue = normalizeAddressInputValue(
    (await input.inputValue().catch(() => "")) || "",
  );
  if (addressInputMatchesQuery(currentValue, query)) {
    await input.dispatchEvent("input").catch(() => undefined);
    await input.dispatchEvent("change").catch(() => undefined);
    if (page) {
      await awaitHumanPacing(page);
      const appeared = await waitForGooglePlacesPredictions(
        page,
        suggestionTimeoutMs,
      );
      if (!appeared && !forEnumeration) {
        await input.click({ timeout: 3_000, force: true }).catch(() => undefined);
        await waitForGooglePlacesPredictions(page, suggestionTimeoutMs);
      }
      if (!forEnumeration) {
        const hasPac = await page
          .locator(".pac-container .pac-item")
          .first()
          .isVisible()
          .catch(() => false);
        if (hasPac) {
          await input.press("ArrowDown").catch(() => undefined);
          await page.waitForTimeout(150);
        }
      }
    }
    return;
  }

  try {
    await input.fill("", { timeout: suggestionTimeoutMs });
    await input.fill(query, { timeout: suggestionTimeoutMs });
  } catch {
    await input.press("Control+A").catch(() => undefined);
    await input.press("Backspace").catch(() => undefined);
    await input.pressSequentially(query, { delay: 25 });
  }

  await input.dispatchEvent("input").catch(() => undefined);
  await input.dispatchEvent("change").catch(() => undefined);
  if (page) {
    await awaitHumanPacing(page);
    const appeared = await waitForGooglePlacesPredictions(
      page,
      suggestionTimeoutMs,
    );
    // 某些地址（如 7000 NW 52nd St, Miami, FL）fill 写值不触发 Google 联想：
    // 逐字符重输产生真实 keydown/input 序列，重新等待联想请求完成
    if (!appeared) {
      await input.click({ timeout: 3_000, force: true }).catch(() => undefined);
      await input.press("Control+A").catch(() => undefined);
      await input.press("Backspace").catch(() => undefined);
      await input.pressSequentially(query, { delay: 60 }).catch(() => undefined);
      await waitForGooglePlacesPredictions(page, suggestionTimeoutMs);
    }
    if (!forEnumeration) {
      const hasPac = await page
        .locator(".pac-container .pac-item")
        .first()
        .isVisible()
        .catch(() => false);
      // MotherShip 主路径为 styled-components 联想；ArrowDown 会把焦点移到 Use my location
      if (hasPac) {
        await input.press("ArrowDown").catch(() => undefined);
        await page.waitForTimeout(150);
      }
    }
  }
}

/** 返回联想是否出现（Google Places 响应、.pac-item、或 styled-components 联想可见） */
async function waitForGooglePlacesPredictions(
  page: Page,
  timeoutMs: number,
): Promise<boolean> {
  return Promise.race([
    // Google Places API 响应
    page
      .waitForResponse(
        (response) =>
          response.url().includes("maps.googleapis.com") &&
          /Autocomplete|Autocompletion|place/i.test(response.url()) &&
          response.status() === 200,
        { timeout: timeoutMs },
      )
      .then(() => true),
    // 标准 pac-container
    page
      .waitForSelector(".pac-container .pac-item", {
        state: "visible",
        timeout: timeoutMs,
      })
      .then(() => true),
    // MotherShip styled-components 联想（轮询检测）
    (async () => {
      const pollInterval = 200;
      const deadline = Date.now() + timeoutMs;
      while (Date.now() < deadline) {
        // 检测 styled-components 联想（特征：包含州缩写的 sc- div）
        const found = await page.evaluate(() => {
          const stateRe = /(USA|US|CA|NY|TX|FL|IL|PA|OH|GA|NC|MI|NJ|VA|WA|AZ|MA|TN|IN|MO|MD|WI|CO|MN|SC|AL|LA|KY|OR|OK|CT|UT|IA|NV|AR|MS|KS|NM|NE|WV|ID|HI|NH|ME|MT|RI|DE|SD|ND|AK|DC|VT|WY|PR)/;
          const skipRe = /use my location|使用我的位置|get a freight|freight details|pick up from|deliver to|where to/i;
          let count = 0;
          document.querySelectorAll("div[class*='sc-']").forEach((el) => {
            const text = (el.textContent || "").replace(/\\s+/g, " ").trim();
            if (text.length >= 15 && text.length <= 150 && text.includes(",") && stateRe.test(text) && !skipRe.test(text)) {
              const rect = (el as HTMLElement).getBoundingClientRect();
              if (rect.width > 300 && rect.height >= 30 && rect.height <= 70) {
                count++;
              }
            }
          });
          return count >= 1;
        }).catch(() => false);
        if (found) return true;
        await page.waitForTimeout(pollInterval);
      }
      return false;
    })(),
  ]).catch(() => false);
}

/** 多策略键入直至 MotherShip 联想出现 */
export async function ensureAddressSuggestions(
  page: Page,
  input: Locator,
  address: AddressLookupInput,
  options?: {
    mothershipLabel?: string;
    suggestionTimeoutMs?: number;
    maxQueries?: number;
    preferConfirmedLabel?: boolean;
  },
): Promise<string> {
  const resolved = resolveAddressAutocompleteInput(address, options);
  const allQueries = buildAddressAutocompleteQueries(address, options);
  const queries = allQueries.slice(
    0,
    options?.maxQueries ??
      (options?.preferConfirmedLabel ? 1 : allQueries.length),
  );
  let lastQuery = queries[0] ?? formatAddressQuery(resolved);
  const suggestionTimeoutMs = resolveAddressSuggestionTimeoutMs(
    options?.suggestionTimeoutMs ?? ADDRESS_SUGGESTION_TIMEOUT_MS,
  );
  const streetNum = queryStreetNumber(resolved);
  let fallbackQuery = "";

  for (const query of queries) {
    lastQuery = query;
    await triggerAddressAutocomplete(input, query, page, {
      suggestionTimeoutMs,
    });
    if (
      await waitForAddressSuggestions(page, resolved, suggestionTimeoutMs)
    ) {
      if (streetNum) {
        const labels = await collectAddressSuggestionLabelsWhenStable(
          page,
          resolved,
          { timeoutMs: Math.min(suggestionTimeoutMs, 5_000) },
        );
        if (labelsContainStreetNumber(labels, streetNum)) {
          return query;
        }
        if (!fallbackQuery) {
          fallbackQuery = query;
        }
        continue;
      }
      return query;
    }
  }

  if (fallbackQuery) {
    return fallbackQuery;
  }

  throw new RpaError(
    "ADDRESS_SUGGESTION_NOT_FOUND",
    `MotherShip 地址联想未出现：${lastQuery}`,
    { retryable: false },
  );
}

async function hasVisibleSuggestionItems(page: Page): Promise<boolean> {
  const roots = [
    page.locator(".pac-container .pac-item"),
    page.locator('[role="listbox"] [role="option"]'),
  ];
  for (const root of roots) {
    const count = await root.count().catch(() => 0);
    if (count > 0) {
      return true;
    }
  }
  return false;
}

export async function waitForAddressSuggestions(
  page: Page,
  query: AddressLookupInput,
  timeoutMs = ADDRESS_SUGGESTION_TIMEOUT_MS,
): Promise<boolean> {
  const deadline = Date.now() + timeoutMs;
  let pass = 0;
  while (Date.now() < deadline) {
    pass += 1;
    const quickHit =
      pass <= 2 || (await hasVisibleSuggestionItems(page));
    if (quickHit) {
      const labels = await collectAddressSuggestionLabels(page, query);
      if (labels.length > 0) {
        return true;
      }
    }
    await page.waitForTimeout(POLL_INTERVAL_MS);
  }
  return false;
}

function suggestionMatchKeys(
  label: string,
  query?: AddressLookupInput,
): string[] {
  const keys: string[] = [];
  const trimmed = label.replace(/\s+/g, " ").trim();

  for (const variant of gluedVariants(trimmed)) {
    if (variant.length >= 8) {
      keys.push(variant.slice(0, Math.min(32, variant.length)));
    }
  }

  if (trimmed.length >= 12) {
    keys.push(trimmed.slice(0, Math.min(40, trimmed.length)));
  }

  const city = query?.city.trim();
  const streetNum = query?.street.match(/^\d+/)?.[0];
  if (streetNum && city && city.includes(" ")) {
    keys.push(`${streetNum} ${city}`);
  }

  return [...new Set(keys)].filter((key) => key.length >= 10);
}

/** 在当前页面下拉中解析与用户确认项最匹配的 DOM 文案 */
export function scoreSuggestionLabelMatch(
  userLabel: string,
  domLabel: string,
  query?: AddressLookupInput,
): number {
  const userVariants = gluedVariants(userLabel).map((v) => v.toLowerCase());
  const dom = normalizeDomSuggestionLabel(domLabel).toLowerCase();
  if (!dom) {
    return 0;
  }

  for (const variant of userVariants) {
    if (variant === dom) {
      return 100;
    }
    if (variant.length >= 12 && dom.includes(variant)) {
      return 100;
    }
    if (dom.length >= 12 && variant.includes(dom)) {
      return 100;
    }
  }

  const streetNum = query?.street.match(/^\d+/)?.[0];
  if (streetNum) {
    if (!dom.includes(streetNum)) {
      return 0;
    }
  } else {
    const streetTokens = (query?.street ?? "")
      .trim()
      .toLowerCase()
      .split(/\s+/)
      .filter((token) => token.length >= 4);
    if (
      streetTokens.length > 0 &&
      !streetTokens.some((token) => dom.includes(token))
    ) {
      return 0;
    }
  }

  let score = 40;
  if (query?.state && dom.includes(query.state.toLowerCase())) {
    score += 20;
  }
  const city = query?.city.trim().toLowerCase();
  if (city && city.length >= 3) {
    if (dom.includes(city)) {
      score += 40;
    } else {
      const cityParts = city.split(/\s+/).filter((part) => part.length >= 3);
      if (
        cityParts.length > 1 &&
        cityParts.every((part) => new RegExp(`\\b${part}\\b`).test(dom))
      ) {
        score += 25;
      } else if (cityParts.length === 1) {
        const token = cityParts[0];
        if (new RegExp(`\\b${token}\\b`).test(dom)) {
          score += 20;
        }
      }
    }
  }
  const streetWord = query?.street.split(/\s+/)[1]?.toLowerCase();
  if (streetWord && streetWord.length >= 3 && dom.includes(streetWord)) {
    score += 15;
  }
  return score;
}

export async function resolveDomSuggestionLabel(
  page: Page,
  userLabel: string,
  query: AddressLookupInput,
): Promise<string | null> {
  const labels = await collectAddressSuggestionLabels(page, query);
  let best: { label: string; score: number } | null = null;

  for (const label of labels) {
    const score = scoreSuggestionLabelMatch(userLabel, label, query);
    if (!best || score > best.score) {
      best = { label, score };
    }
  }

  return best && best.score >= 50 ? best.label : null;
}

/** 用键盘在联想列表中选中评分最高的项 */
export async function selectSuggestionByKeyboard(
  page: Page,
  input: Locator,
  userLabel: string,
  query: AddressLookupInput,
): Promise<boolean> {
  const labels = await collectAddressSuggestionLabels(page, query);
  let bestIndex = -1;
  let bestScore = 0;
  labels.forEach((domLabel, index) => {
    const score = scoreSuggestionLabelMatch(userLabel, domLabel, query);
    if (score > bestScore) {
      bestScore = score;
      bestIndex = index;
    }
  });
  if (bestIndex < 0 || bestScore < 50) {
    return false;
  }

  await input.click({ force: true }).catch(() => undefined);
  await page.waitForTimeout(150);
  for (let i = 0; i <= bestIndex; i += 1) {
    await page.keyboard.press("ArrowDown");
    await page.waitForTimeout(120);
  }
  await page.keyboard.press("Enter");
  await page.waitForTimeout(200);
  return true;
}

/** 定位用户确认的联想项（评分 + 联想容器内查找，避免短键误点） */
export async function findAddressSuggestionLocator(
  page: Page,
  label: string,
  query?: AddressLookupInput,
): Promise<Locator | null> {
  if (query) {
    const labels = await collectAddressSuggestionLabels(page, query);
    let best: { label: string; score: number } | null = null;
    for (const domLabel of labels) {
      const score = scoreSuggestionLabelMatch(label, domLabel, query);
      if (!best || score > best.score) {
        best = { label: domLabel, score };
      }
    }
    if (best && best.score >= 50) {
      const scored = await locateSuggestionByLabelText(page, best.label);
      if (scored) {
        return scored;
      }
    }
  }

  const specs = getSelectorSpecs("RPA_SELECTOR_ADDRESS_SUGGESTION");
  for (const spec of specs) {
    const loc = pageLocator(page, spec).filter({ hasText: label }).first();
    if (await loc.isVisible().catch(() => false)) {
      return loc;
    }
  }

  for (const variant of gluedVariants(label)) {
    const located = await locateSuggestionByLabelText(page, variant);
    if (located) {
      return located;
    }
  }

  const widget = getQuoteWidgetLocator(page);
  for (const key of suggestionMatchKeys(label, query)) {
    const partial = widget.getByText(key, { exact: false }).first();
    if (await partial.isVisible().catch(() => false)) {
      return partial;
    }
  }

  return null;
}

/** Google Places 打开联想时默认高亮第 0 项；index 即所需 ArrowDown 次数 */
export function arrowDownCountForPacIndex(index: number): number {
  return Math.max(0, index);
}

async function selectSuggestionByBoundIndex(
  page: Page,
  input: Locator,
  index: number,
  options?: { alreadyHighlightedFirst?: boolean },
): Promise<void> {
  await input.focus().catch(() => input.click({ force: true }).catch(() => undefined));
  await page.waitForTimeout(100);

  const highlighted = options?.alreadyHighlightedFirst ?? true;
  const arrowDownCount = highlighted
    ? arrowDownCountForPacIndex(index)
    : index + 1;
  for (let i = 0; i < arrowDownCount; i += 1) {
    await page.keyboard.press("ArrowDown");
    await page.waitForTimeout(80);
  }
  await page.keyboard.press("Enter");
  await page.waitForTimeout(200);
}

async function clickPacItemAtIndex(page: Page, index: number): Promise<boolean> {
  const items = page.locator(".pac-container .pac-item");
  const count = await items.count().catch(() => 0);
  if (index < 0 || index >= count) {
    return false;
  }
  const item = items.nth(index);
  if (!(await item.isVisible().catch(() => false))) {
    return false;
  }
  const queryLine = item.locator(".pac-item-query").first();
  const target =
    (await queryLine.isVisible().catch(() => false)) ? queryLine : item;
  await clickLocatedSuggestionWithPointer(page, target);
  return true;
}

/** 在页面上下文派发 pac-item 完整鼠标事件（绕过 Playwright click 未触发 handler 的情况） */
async function dispatchPacItemSelectionEvents(
  page: Page,
  index: number,
): Promise<boolean> {
  return page.evaluate((idx) => {
    const items = document.querySelectorAll(".pac-container .pac-item");
    const item = items[idx] as HTMLElement | undefined;
    if (!item) {
      return false;
    }
    item.scrollIntoView({ block: "center" });
    const target = (item.querySelector(".pac-item-query") ??
      item) as HTMLElement;
    for (const type of ["mousedown", "mouseup", "click"] as const) {
      target.dispatchEvent(
        new MouseEvent(type, {
          bubbles: true,
          cancelable: true,
          view: window,
        }),
      );
    }
    return true;
  }, index);
}

async function commitSelectionViaKeyboardTab(
  page: Page,
  input: Locator,
  index: number,
): Promise<void> {
  await selectSuggestionByBoundIndex(page, input, index, {
    alreadyHighlightedFirst: true,
  });
  await page.keyboard.press("Tab");
  await page.waitForTimeout(250);
}

async function clickListboxOptionAtIndex(page: Page, index: number): Promise<boolean> {
  const roots = [
    page.locator('[role="listbox"] [role="option"]'),
    getQuoteWidgetLocator(page).locator('[role="listbox"] [role="option"]'),
  ];
  for (const root of roots) {
    const count = await root.count().catch(() => 0);
    if (index < 0 || index >= count) {
      continue;
    }
    const item = root.nth(index);
    if (!(await item.isVisible().catch(() => false))) {
      continue;
    }
    await item.scrollIntoViewIfNeeded().catch(() => undefined);
    await item.click({ timeout: 3_000 });
    return true;
  }
  return false;
}

async function selectSuggestionBySearchIndex(
  page: Page,
  input: Locator,
  index: number,
): Promise<void> {
  await input.click({ force: true }).catch(() => undefined);
  await page.waitForTimeout(150);
  for (let i = 0; i < arrowDownCountForPacIndex(index); i += 1) {
    await page.keyboard.press("ArrowDown");
    await page.waitForTimeout(120);
  }
  await page.keyboard.press("Enter");
  await page.waitForTimeout(200);
}

/** 按 axel search 返回的 description 点选（绑定 placeId，非纯 DOM label） */
async function clickSuggestionBySearchDescription(
  page: Page,
  description: string,
  query?: AddressLookupInput,
): Promise<boolean> {
  const probes = [
    description,
    stripCountrySuffix(description),
    description.split(",")[0]?.trim() ?? "",
    ...gluedVariants(description),
  ].filter((value, index, arr) => value.length >= 8 && arr.indexOf(value) === index);

  for (const probe of probes) {
    const located = await locateSuggestionByLabelText(page, probe);
    if (!located) {
      continue;
    }
    const score = scoreSuggestionLabelMatch(description, probe, query);
    if (score < 50) {
      continue;
    }
    await located.scrollIntoViewIfNeeded().catch(() => undefined);
    await located.hover({ force: true }).catch(() => undefined);
    await located.click({ force: true, timeout: 3_000 }).catch(() => undefined);
    return true;
  }
  return false;
}

async function clickLocatedSuggestionWithPointer(
  page: Page,
  located: Locator,
): Promise<void> {
  await located.scrollIntoViewIfNeeded().catch(() => undefined);
  const box = await located.boundingBox().catch(() => null);
  if (box) {
    const x = box.x + box.width / 2;
    const y = box.y + box.height / 2;
    await page.mouse.move(x, y);
    await page.mouse.down();
    await page.waitForTimeout(60);
    await page.mouse.up();
    await page.waitForTimeout(120);
    return;
  }
  await located.click({ force: true, timeout: 3_000 }).catch(() => undefined);
}

/** 按 description 文本 pointer 点选（优先于 index 键盘，避免 pac 顺序与 axel search 不一致） */
async function clickSuggestionByDescriptionPointer(
  page: Page,
  description: string,
  query?: AddressLookupInput,
): Promise<boolean> {
  const probes = [
    description,
    stripCountrySuffix(description),
    description.split(",")[0]?.trim() ?? "",
    ...gluedVariants(description),
  ].filter((value, index, arr) => value.length >= 8 && arr.indexOf(value) === index);

  for (const probe of probes) {
    const located = await locateSuggestionByLabelText(page, probe);
    if (!located) {
      continue;
    }
    const score = scoreSuggestionLabelMatch(description, probe, query);
    if (score < 50) {
      continue;
    }
    await clickLocatedSuggestionWithPointer(page, located);
    return true;
  }
  return false;
}

export async function ensureSuggestionDropdownOpen(
  page: Page,
  input: Locator,
  query: AddressLookupInput,
  fallbackText: string,
): Promise<void> {
  const resolved = resolveAddressAutocompleteInput(query, {
    mothershipLabel: fallbackText,
  });
  const probes = [
    fallbackText,
    stripCountrySuffix(fallbackText),
    `${resolved.street}, ${resolved.city}, ${resolved.state}`,
    `${resolved.street}, ${resolved.city}, ${resolved.state}, USA`,
  ].filter((value, index, arr) => value.length >= 8 && arr.indexOf(value) === index);

  await input.click({ force: true }).catch(() => undefined);
  await page.waitForTimeout(150);
  if (await waitForStyledSuggestionRows(page, probes, resolved, 1_500)) {
    return;
  }
  if (await waitForAddressSuggestions(page, resolved, 2_000)) {
    return;
  }
  const hint = `${resolved.street}, ${resolved.city}, ${resolved.state}`.trim();
  const currentValue = normalizeAddressInputValue(
    (await input.inputValue().catch(() => "")) || "",
  );
  if (
    addressInputMatchesQuery(currentValue, hint) ||
    addressInputMatchesQuery(currentValue, fallbackText)
  ) {
    await waitForStyledSuggestionRows(page, probes, resolved, 4_000);
    if (await waitForAddressSuggestions(page, resolved, 2_000)) {
      return;
    }
  }
  if (addressInputMatchesQuery(currentValue, hint)) {
    return;
  }
  await input.fill("").catch(() => undefined);
  await input.pressSequentially(hint, { delay: 45 }).catch(() => undefined);
  await waitForGooglePlacesPredictions(page, 8_000);
  await waitForStyledSuggestionRows(page, probes, resolved, 4_000);
}

const AXEL_PLACE_COMMIT_TIMEOUT_MS = 12_000;
const AXEL_PLACE_COMMIT_QUICK_MS = 5_000;

// #region agent log
function debugSelectLog(
  hypothesisId: string,
  location: string,
  message: string,
  data: Record<string, unknown>,
): void {
  fetch("http://127.0.0.1:7489/ingest/ff976922-7b71-4f91-ad84-5521d8ba1420", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-Debug-Session-Id": "efb842",
    },
    body: JSON.stringify({
      sessionId: "efb842",
      hypothesisId,
      location,
      message,
      data,
      timestamp: Date.now(),
    }),
  }).catch(() => undefined);
}
// #endregion

/**
 * search → placeId 绑定 → 点选 → 等待 GET place/{id} 200
 * 禁止无 placeId 的 selection(label)。
 */
function widgetShowsQueryAddress(
  widgetText: string,
  query: AddressLookupInput,
): boolean {
  const w = widgetText.replace(/\s+/g, " ").toLowerCase();
  const num = query.street.match(/^\d+/)?.[0];
  if (num && !w.includes(num.toLowerCase())) {
    return false;
  }
  const state = query.state?.trim().toLowerCase();
  if (state && !w.includes(state)) {
    return false;
  }
  const city = query.city?.trim().toLowerCase();
  if (city && city.length >= 3 && !w.includes(city)) {
    return false;
  }
  return true;
}

async function readStreetInputValuesInWidget(
  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[]>;
}

/** 提货 commit 后须释放独立派送框（单框仍含提货全文则未锁定） */
async function isDeliveryInputUnlocked(
  page: Page,
  widgetSelector: string,
  query: AddressLookupInput,
): Promise<boolean> {
  const values = await readStreetInputValuesInWidget(page, widgetSelector);
  if (values.length >= 2) {
    return true;
  }
  if (values.length === 1) {
    const only = values[0]!.replace(/\s+/g, " ").trim().toLowerCase();
    const streetNum = query.street.match(/^\d+/)?.[0]?.toLowerCase();
    if (streetNum && only.includes(streetNum) && only.length >= 8) {
      return false;
    }
    return only.length < 8;
  }
  return true;
}

export async function selectAddressWithPlaceBinding(
  page: Page,
  bridge: AxelSelectionBridge,
  selection: AxelSelectionPayload,
  query: AddressLookupInput,
  input: Locator,
  options?: {
    side?: "pickup" | "delivery";
    widgetSelector?: string;
    pickupAnchor?: AddressLookupInput;
    confirmedAddress?: QuoteRequest["pickup"];
    ctx?: RPAContext;
  },
): Promise<void> {
  if (!selection.placeId?.trim()) {
    throw new RpaError(
      "ADDRESS_NOT_CONFIRMED",
      "地址选择缺少 placeId，无法触发 MotherShip commit",
      { retryable: false },
    );
  }

  const index = bridge.getCandidateIndex(selection.placeId);
  if (index < 0) {
    throw new RpaError(
      "ADDRESS_SUGGESTION_NOT_FOUND",
      `search 绑定 placeId 不在候选列表：${selection.placeId}`,
      { retryable: false },
    );
  }

  const attemptPaths: string[] = [];

  await ensureSuggestionDropdownOpen(page, input, query, selection.description);

  const traceSide = options?.side ?? "rpa";
  const preCommit = await captureSelectionTrace(page, "pre-commit-attempts");
  logSelectionTrace(traceSide, preCommit);

  const normalizedPlaceId = selection.placeId;

  const validateWidgetCommit = async (path: string): Promise<boolean> => {
    if (!options?.widgetSelector) {
      return true;
    }
    if (options.side === "pickup" && options.confirmedAddress) {
      const locked = await isPickupReadyForDeliveryInput(
        page,
        options.widgetSelector,
        options.confirmedAddress,
      );
      if (!locked) {
        console.log(
          `[rpa] address-select: pickup widget 未锁定 path=${path}`,
        );
        return false;
      }
      return true;
    }
    const widgetText = await page
      .locator(options.widgetSelector)
      .innerText()
      .catch(() => "");
    if (!widgetShowsQueryAddress(widgetText, query)) {
      console.log(
        `[rpa] address-select: widget 未体现 ${options.side ?? "side"} 地址 path=${path}`,
      );
      return false;
    }
    if (options.side === "delivery" && options.pickupAnchor) {
      if (!widgetShowsQueryAddress(widgetText, options.pickupAnchor)) {
        console.log(
          `[rpa] address-select: 派送 commit 后提货从 widget 消失 path=${path}`,
        );
        return false;
      }
    }
    return true;
  };

  const tryCommit = async (path: string, quickMs = AXEL_PLACE_COMMIT_QUICK_MS) => {
    attemptPaths.push(path);
    if (!(await bridge.waitForPlaceCommit(normalizedPlaceId, quickMs))) {
      return false;
    }
    if (!(await validateWidgetCommit(path))) {
      return false;
    }
    await page.keyboard.press("Escape").catch(() => undefined);
    await page.keyboard.press("Tab").catch(() => undefined);
    await page.waitForTimeout(200);
    debugSelectLog(
      "H2",
      "address-suggestions.ts:selectAddressWithPlaceBinding",
      "commit-ok",
      { clickPath: attemptPaths.join(" → "), placeId: normalizedPlaceId },
    );
    return true;
  };

  const styledClick = await clickMothershipStyledSuggestion(
    page,
    selection.description,
    query,
    { input, candidateIndex: index },
  );
  if (styledClick.clicked && (await tryCommit("mothership-styled-click"))) {
    return;
  }
  if (styledClick.clicked) {
    console.log(
      `[rpa] mothership-styled-click: 已点选但未观测 place 请求 matched=${styledClick.matchedText?.slice(0, 48) ?? "—"}`,
    );
    await ensureSuggestionDropdownOpen(page, input, query, selection.description);
  }

  if (
    (await commitViaAxelPlacePrefetch(
      page,
      bridge,
      normalizedPlaceId,
      selection.description,
      input,
      { query, candidateIndex: index },
    ).then((r) => r.ok)) &&
    (await tryCommit("axel-place-prefetch"))
  ) {
    return;
  }

  const preKeyboard = await captureSelectionTrace(
    page,
    `pre-keyboard-bound-${index}`,
  );
  logSelectionTrace(traceSide, preKeyboard);
  const pacVisible = await page
    .locator(".pac-container .pac-item")
    .first()
    .isVisible()
    .catch(() => false);
  if (pacVisible) {
    await selectSuggestionByBoundIndex(page, input, index, {
      alreadyHighlightedFirst: true,
    });
    if (await tryCommit(`keyboard-bound-${index}`)) {
      return;
    }
  }
  const postKeyboard = await captureSelectionTrace(
    page,
    `post-keyboard-bound-${index}`,
  );
  logSelectionTrace(traceSide, postKeyboard);

  debugSelectLog("H2", "address-suggestions.ts:selectAddressWithPlaceBinding", "after-keyboard", {
    clickPath: attemptPaths.join(" → "),
    placeId: normalizedPlaceId,
    description: selection.description.slice(0, 80),
    candidateIndex: index,
  });

  if (
    (await clickListboxOptionAtIndex(page, index)) &&
    (await tryCommit(`listbox-${index}`))
  ) {
    return;
  }

  await ensureSuggestionDropdownOpen(page, input, query, selection.description);
  await commitSelectionViaKeyboardTab(page, input, index);
  if (await tryCommit(`keyboard-tab-${index}`)) {
    return;
  }

  await ensureSuggestionDropdownOpen(page, input, query, selection.description);
  await selectSuggestionBySearchIndex(page, input, index);
  if (await tryCommit(`keyboard-full-${index}`)) {
    return;
  }

  if (
    (await clickSuggestionByDescriptionPointer(
      page,
      selection.description,
      query,
    )) &&
    (await tryCommit("description-pointer"))
  ) {
    return;
  }

  if (
    (await clickSuggestionBySearchDescription(
      page,
      selection.description,
      query,
    )) &&
    (await tryCommit("description-click"))
  ) {
    return;
  }

  if (
    (await dispatchPacItemSelectionEvents(page, index)) &&
    (await tryCommit(`pac-dispatch-${index}`))
  ) {
    return;
  }

  if (
    (await clickPacItemAtIndex(page, index)) &&
    (await tryCommit(`pac-item-${index}`))
  ) {
    return;
  }

  await ensureSuggestionDropdownOpen(page, input, query, selection.description);
  if (
    (await clickSuggestionByDescriptionPointer(
      page,
      selection.description,
      query,
    )) &&
    (await tryCommit("description-pointer-retry"))
  ) {
    return;
  }

  const diag = bridge.getPlaceCommitDiagnostics(normalizedPlaceId);
  const reason = formatPlaceCommitFailure(
    normalizedPlaceId,
    attemptPaths,
    diag,
  );
  console.log(`[rpa] axel-place-gate: ${reason}`);
  debugSelectLog(
    "H2",
    "address-suggestions.ts:selectAddressWithPlaceBinding",
    "commit-failed",
    { clickPath: attemptPaths.join(" → "), diag, reason },
  );
  throw new RpaError("ADDRESS_NOT_CONFIRMED", reason, { retryable: true });
}

/** MotherShip styled-components 联想项：按评分点击单行 */
export async function clickSuggestionByLabel(
  page: Page,
  label: string,
  query?: AddressLookupInput,
): Promise<boolean> {
  const labels = query
    ? await collectAddressSuggestionLabels(page, query)
    : [label];
  let bestLabel = label;
  let bestScore = 0;
  for (const domLabel of labels) {
    const score = scoreSuggestionLabelMatch(label, domLabel, query);
    if (score > bestScore) {
      bestScore = score;
      bestLabel = domLabel;
    }
  }
  if (bestScore < 50) {
    return false;
  }

  const roots = [
    page.locator(".pac-container .pac-item"),
    page.locator('[role="listbox"] [role="option"]'),
    page.locator("div[class*='sc-']"),
  ];

  let target: Locator | null = null;
  let targetScore = 0;
  for (const root of roots) {
    const count = await root.count().catch(() => 0);
    for (let i = 0; i < count; i += 1) {
      const item = root.nth(i);
      const text = ((await item.innerText().catch(() => "")) || "")
        .replace(/\s+/g, " ")
        .trim();
      if (!text) {
        continue;
      }
      const box = await item.boundingBox().catch(() => null);
      if (!box || box.width < 200 || box.height < 24 || box.height > 80) {
        continue;
      }
      const score = scoreSuggestionLabelMatch(label, text, query);
      if (score > targetScore) {
        targetScore = score;
        target = item;
      }
    }
  }

  if (!target || targetScore < bestScore - 5) {
    return false;
  }

  await target.scrollIntoViewIfNeeded().catch(() => undefined);
  await target.hover({ force: true }).catch(() => undefined);
  await target.click({ force: true, timeout: 3_000 }).catch(() => undefined);
  return true;
}

/** 读取已填入/锁定的地址字段文本（排除联想下拉 innerText 污染） */
export async function getAddressConfirmationSurfaceText(
  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 >= 4) chunks.push(val);
      }
      return chunks.join(" ");
    })(${JSON.stringify(widgetSelector)})`,
  ) as Promise<string>;
}

/** 地址联想下拉是否仍打开（勿用 getByText 判断，输入框回显会误判） */
export async function isAddressSuggestionDropdownOpen(
  page: Page,
): Promise<boolean> {
  const roots = [
    page.locator(".pac-container"),
    page.locator('[role="listbox"]'),
    getQuoteWidgetLocator(page).locator('[role="listbox"]'),
  ];

  for (const root of roots) {
    const count = await root.count().catch(() => 0);
    for (let i = 0; i < count; i += 1) {
      if (await root.nth(i).isVisible().catch(() => false)) {
        return true;
      }
    }
  }

  const styledOpen = (await page.evaluate(`
    (function() {
      var stateRe = /,\\s*(USA|US|CA|NY|TX|FL|WA|[A-Z]{2})\\b/;
      var nodes = document.querySelectorAll("div[class*='sc-']");
      for (var i = 0; i < nodes.length; i++) {
        var el = nodes[i];
        var text = (el.innerText || "").replace(/\\s+/g, " ").trim();
        if (text.length < 15 || !stateRe.test(text)) continue;
        var rect = el.getBoundingClientRect();
        if (
          rect.width > 180 &&
          rect.height >= 24 &&
          rect.height <= 100 &&
          el.offsetParent !== null
        ) {
          return true;
        }
      }
      return false;
    })()
  `)) as boolean;

  return styledOpen;
}

/** 地址输入框是否已写入确认地址（styled-components 下拉检测不可靠时的兜底） */
export async function isAddressInputCommitted(
  input: Locator,
  address: AddressLookupInput,
): Promise<boolean> {
  const value = ((await input.inputValue().catch(() => "")) || "")
    .replace(/\s+/g, " ")
    .trim()
    .toLowerCase();
  if (value.length < 8) {
    return false;
  }
  const streetNum = address.street.match(/^\d+/)?.[0];
  if (streetNum && !value.includes(streetNum)) {
    return false;
  }
  const city = address.city.trim().toLowerCase();
  if (city && !value.includes(city)) {
    const last = city.split(/\s+/).pop();
    if (!last || !value.includes(last)) {
      return false;
    }
  }
  const state = address.state.trim().toLowerCase();
  return !state || value.includes(state);
}

/** 点选后等待联想下拉收起 */
export async function waitForAddressSuggestionLocked(
  page: Page,
  timeoutMs = 3_000,
): Promise<boolean> {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    if (!(await isAddressSuggestionDropdownOpen(page))) {
      return true;
    }
    await page.waitForTimeout(100);
  }
  return false;
}

/**
 * 通过街道文字直接找到建议行并点击（Playwright codegen 验证方案）
 * 不依赖 styled-component 尺寸检测，直接用 page.getByText 匹配第一条可见建议
 */
export async function clickFirstVisibleSuggestionByText(
  page: Page,
  streetAddress: string,
): Promise<boolean> {
  // 取街道号+街道名前缀（去掉城市、州），作为 partial match
  const street = streetAddress.split(",")[0]?.trim() ?? streetAddress;
  if (!street) return false;

  // 找所有包含街道文字的可见元素，排除 input/textarea 本身
  const candidates = page.locator(
    `:not(input):not(textarea):not(label):not(button)`,
  ).filter({ hasText: street });

  const count = await candidates.count().catch(() => 0);
  for (let i = 0; i < count; i++) {
    const el = candidates.nth(i);
    if (!(await el.isVisible().catch(() => false))) continue;
    const box = await el.boundingBox().catch(() => null);
    if (!box || box.width < 100 || box.height < 20 || box.height > 200) continue;
    // 排除整个 widget 容器（面积过大）
    if (box.width > 900 || box.height > 300) continue;
    await el.click({ force: true, timeout: 3_000 }).catch(() => undefined);
    console.log(
      `[rpa] click-suggestion-by-text: clicked "${street.slice(0, 40)}" box=${Math.round(box.width)}x${Math.round(box.height)}`,
    );
    return true;
  }
  return false;
}
