import type { Page, Response } from "playwright";
import type { AddressLookupInput } from "@/modules/address/types";
import { scoreSuggestionLabelMatch } from "@/workers/rpa/address-suggestions";

const AXEL_SEARCH_URL = "services.mothership.com/axel/location/search";
const AXEL_PLACE_PREFIX = "services.mothership.com/axel/location/place/";
const MIN_LABEL_MATCH_SCORE = 50;
const GOOGLE_PLACE_ID_STD_LEN = 27;
const GOOGLE_PLACE_ID_STD_RE = /^ChIJ[A-Za-z0-9_-]{23}$/;
const GOOGLE_PLACE_ID_LOOSE_RE = /ChIJ[A-Za-z0-9_-]+/;

function extractStandardPlaceId(text: string): string | null {
  const idx = text.indexOf("ChIJ");
  if (idx < 0) {
    return null;
  }
  const slice = text.slice(idx, idx + GOOGLE_PLACE_ID_STD_LEN);
  return GOOGLE_PLACE_ID_STD_RE.test(slice) ? slice : null;
}

function decodeEmbeddedPlaceId(trimmed: string): string | null {
  const fromText = extractStandardPlaceId(trimmed);
  if (fromText) {
    return fromText;
  }
  try {
    const b64 = trimmed.replace(/-/g, "+").replace(/_/g, "/");
    const pad = b64 + "=".repeat((4 - (b64.length % 4)) % 4);
    const decoded = Buffer.from(pad, "base64").toString("latin1");
    return extractStandardPlaceId(decoded);
  } catch {
    return null;
  }
}

/** MotherShip search 有时返回 base64 复合 placeId，commit URL 使用内嵌 27 字符 ChIJ… */
export function normalizeAxelPlaceId(raw: string): string {
  const trimmed = raw.trim();
  if (!trimmed) {
    return trimmed;
  }

  const embedded = decodeEmbeddedPlaceId(trimmed);
  if (embedded) {
    return embedded;
  }

  const loose = trimmed.match(GOOGLE_PLACE_ID_LOOSE_RE);
  if (loose) {
    const trunc = loose[0]!.slice(0, GOOGLE_PLACE_ID_STD_LEN);
    if (GOOGLE_PLACE_ID_STD_RE.test(trunc)) {
      return trunc;
    }
    return loose[0]!;
  }
  return trimmed;
}

/** GET place 候选：先 ChIJ 标准 id，再原始复合 id */
export function axelPlaceFetchCandidates(raw: string): string[] {
  const trimmed = raw.trim();
  if (!trimmed) {
    return [];
  }
  const normalized = normalizeAxelPlaceId(trimmed);
  return normalized === trimmed ? [trimmed] : [normalized, trimmed];
}

export type AxelPlaceCandidate = {
  placeId: string;
  description: string;
  mainText?: string;
  secondaryText?: string;
};

export type AxelSelectionPayload = {
  label: string;
  placeId: string;
  description: string;
};

type SearchResponseBody = {
  googlePlacesResults?: Array<{
    placeId?: string;
    description?: string;
    mainText?: string;
    secondaryText?: string;
  }>;
};

export type AxelPlaceNetworkEvent = {
  placeId: string;
  status: number;
  url: string;
  at: number;
};

// #region agent log
function debugAxelLog(
  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

export function formatPlaceCommitFailure(
  expectedPlaceId: string,
  attemptPaths: string[],
  diag: {
    committedPlaceIds: string[];
    placeNetworkLog: AxelPlaceNetworkEvent[];
  },
): string {
  const paths = attemptPaths.join(" → ") || "none";
  const { placeNetworkLog, committedPlaceIds } = diag;

  if (placeNetworkLog.length === 0) {
    return [
      `place/${expectedPlaceId} commit 失败`,
      "原因：widget 未发起 GET place 请求",
      "说明：点选未触发 MotherShip selection handler（可能仅更新了 input 文本）",
      `路径：${paths}`,
    ].join("；");
  }

  const forExpected = placeNetworkLog.filter(
    (event) => event.placeId === expectedPlaceId,
  );
  if (forExpected.length === 0) {
    const actual = placeNetworkLog
      .map((event) => `${event.placeId.slice(0, 20)}:${event.status}`)
      .join(", ");
    return [
      `place/${expectedPlaceId} commit 失败`,
      "原因：placeId 不匹配",
      `widget 实际请求：${actual}`,
      `路径：${paths}`,
    ].join("；");
  }

  const nonOk = forExpected.filter((event) => event.status !== 200);
  if (nonOk.length > 0) {
    return [
      `place/${expectedPlaceId} commit 失败`,
      `原因：HTTP ${nonOk.map((event) => event.status).join(",")}`,
      `路径：${paths}`,
    ].join("；");
  }

  if (!committedPlaceIds.includes(expectedPlaceId)) {
    return [
      `place/${expectedPlaceId} commit 失败`,
      "原因：等待 place 200 超时",
      `路径：${paths}`,
    ].join("；");
  }

  return `place/${expectedPlaceId} commit 失败；路径：${paths}`;
}

/** search response → placeId 绑定 → commit 网络验签 */
export class AxelSelectionBridge {
  private candidates: AxelPlaceCandidate[] = [];

  private committedPlaceIds = new Set<string>();

  private responseHandler: ((response: Response) => void) | null = null;

  private attachedPage: Page | null = null;

  private placeNetworkLog: AxelPlaceNetworkEvent[] = [];

  private searchIngestCount = 0;

  attach(page: Page): void {
    if (this.attachedPage === page) {
      return;
    }
    this.detach();
    this.attachedPage = page;
    this.responseHandler = (response: Response) => {
      void this.handleResponse(response);
    };
    page.on("response", this.responseHandler);
    // #region agent log
    debugAxelLog("H1", "axel-selection-bridge.ts:attach", "bridge-attached", {
      pageUrl: page.url(),
    });
    // #endregion
  }

  detach(): void {
    if (this.attachedPage && this.responseHandler) {
      this.attachedPage.off("response", this.responseHandler);
    }
    this.attachedPage = null;
    this.responseHandler = null;
  }

  clearCandidates(): void {
    this.candidates = [];
    this.placeNetworkLog = [];
    this.searchIngestCount = 0;
  }

  getPlaceCommitDiagnostics(expectedPlaceId: string): {
    expectedPlaceId: string;
    committedPlaceIds: string[];
    placeNetworkLog: AxelPlaceNetworkEvent[];
    searchCandidateCount: number;
    searchIngestCount: number;
  } {
    return {
      expectedPlaceId,
      committedPlaceIds: [...this.committedPlaceIds],
      placeNetworkLog: [...this.placeNetworkLog],
      searchCandidateCount: this.candidates.length,
      searchIngestCount: this.searchIngestCount,
    };
  }

  /** 供单测与录证脚本注入 search 结果 */
  ingestSearchResults(body: SearchResponseBody): void {
    const rows = body.googlePlacesResults ?? [];
    for (const row of rows) {
      const placeId = row.placeId?.trim();
      const description = row.description?.trim();
      if (!placeId || !description) {
        continue;
      }
      this.candidates.push({
        placeId: normalizeAxelPlaceId(placeId),
        description,
        mainText: row.mainText?.trim(),
        secondaryText: row.secondaryText?.trim(),
      });
    }
  }

  private placeDetailsById = new Map<string, Record<string, unknown>>();

  setPlaceDetails(placeId: string, location: Record<string, unknown>): void {
    const normalized = normalizeAxelPlaceId(placeId);
    if (normalized) {
      this.placeDetailsById.set(normalized, location);
    }
  }

  getPlaceDetails(placeId: string): Record<string, unknown> | undefined {
    return this.placeDetailsById.get(normalizeAxelPlaceId(placeId));
  }

  /** 供单测注入 place commit */
  recordPlaceCommit(placeId: string): void {
    const normalized = normalizeAxelPlaceId(placeId);
    if (normalized) {
      this.committedPlaceIds.add(normalized);
    }
  }

  /** Node 侧 GET place 成功但 widget 未触发 commit 时合成网络验签 */
  recordSyntheticPlaceCommit(placeId: string, status = 200): void {
    const normalized = normalizeAxelPlaceId(placeId);
    if (!normalized) {
      return;
    }
    this.committedPlaceIds.add(normalized);
    this.placeNetworkLog.push({
      placeId: normalized,
      status,
      url: `${AXEL_PLACE_PREFIX}${normalized}?synthetic=1`,
      at: Date.now(),
    });
  }

  getCandidates(): readonly AxelPlaceCandidate[] {
    return this.candidates;
  }

  hasSearchResults(): boolean {
    return this.candidates.length > 0;
  }

  hasPlaceCommit(placeId: string): boolean {
    return this.committedPlaceIds.has(normalizeAxelPlaceId(placeId));
  }

  /** 诊断：bridge 已观测到的 place commit placeId 列表 */
  getObservedPlaceCommitIds(): string[] {
    return [...this.committedPlaceIds];
  }

  /** 是否观测到 GET place/{placeId} 且 HTTP 200 */
  hasSuccessfulPlaceNetworkRequest(placeId: string): boolean {
    const normalized = normalizeAxelPlaceId(placeId);
    return this.placeNetworkLog.some(
      (event) => event.placeId === normalized && event.status === 200,
    );
  }

  getPlaceNetworkLog(): readonly AxelPlaceNetworkEvent[] {
    return [...this.placeNetworkLog];
  }

  /** label → { label, placeId, description }，禁止无 placeId 的纯 DOM 选择 */
  resolveSelection(
    label: string,
    query?: AddressLookupInput,
  ): AxelSelectionPayload | null {
    let best: { candidate: AxelPlaceCandidate; score: number } | null = null;

    let pool = this.candidates;
    const streetNum = query?.street.match(/^\d+/)?.[0];
    if (streetNum) {
      const numbered = pool.filter((candidate) => {
        const texts = [candidate.description, candidate.mainText].filter(
          (text): text is string => Boolean(text?.trim()),
        );
        return texts.some((text) => text.includes(streetNum));
      });
      if (numbered.length > 0) {
        pool = numbered;
      }
    }

    for (const candidate of pool) {
      const probeTexts = [
        candidate.description,
        candidate.mainText && candidate.secondaryText
          ? `${candidate.mainText}, ${candidate.secondaryText}`
          : null,
        candidate.mainText,
      ].filter((text): text is string => Boolean(text?.trim()));

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

    if (!best || best.score < MIN_LABEL_MATCH_SCORE) {
      return null;
    }

    return {
      label,
      placeId: best.candidate.placeId,
      description: best.candidate.description,
    };
  }

  /** 等待 axel/location/search 结果被 bridge 捕获 */
  async waitForSearchResults(timeoutMs: number): Promise<boolean> {
    if (this.candidates.length > 0) {
      return true;
    }
    const deadline = Date.now() + timeoutMs;
    while (Date.now() < deadline) {
      if (this.candidates.length > 0) {
        return true;
      }
      await new Promise((resolve) => setTimeout(resolve, 100));
    }
    return false;
  }

  getCandidateIndex(placeId: string): number {
    const normalized = normalizeAxelPlaceId(placeId);
    return this.candidates.findIndex((item) => item.placeId === normalized);
  }

  async waitForPlaceCommit(
    placeId: string,
    timeoutMs: number,
  ): Promise<boolean> {
    const normalized = normalizeAxelPlaceId(placeId);
    if (this.hasPlaceCommit(normalized)) {
      // #region agent log
      debugAxelLog("H5", "axel-selection-bridge.ts:waitForPlaceCommit", "already-committed", {
        placeId,
      });
      // #endregion
      return true;
    }
    const page = this.attachedPage;
    if (!page) {
      return false;
    }

    const placeUrl = `${AXEL_PLACE_PREFIX}${normalized}`;
    const raced = await Promise.race([
      page
        .waitForResponse(
          (response) => {
            const url = response.url();
            return (
              url.includes(AXEL_PLACE_PREFIX) &&
              (url.includes(normalized) || url.includes(encodeURIComponent(normalized))) &&
              response.status() === 200
            );
          },
          { timeout: timeoutMs },
        )
        .then((response) => {
          this.committedPlaceIds.add(normalized);
          return response.status() === 200;
        }),
      new Promise<boolean>((resolve) => {
        const deadline = Date.now() + timeoutMs;
        const poll = (): void => {
          if (this.hasPlaceCommit(normalized)) {
            resolve(true);
            return;
          }
          if (Date.now() >= deadline) {
            resolve(false);
            return;
          }
          setTimeout(poll, 100);
        };
        poll();
      }),
    ]).catch(() => false);

    // #region agent log
    debugAxelLog("H2", "axel-selection-bridge.ts:waitForPlaceCommit", "wait-finished", {
      placeId: normalized,
      rawPlaceId: placeId.slice(0, 48),
      committed: raced,
      timeoutMs,
      diagnostics: this.getPlaceCommitDiagnostics(placeId),
    });
    // #endregion

    return raced;
  }

  private recordPlaceNetwork(url: string, status: number): void {
    if (!url.includes(AXEL_PLACE_PREFIX) || url.includes("reverse-geocode")) {
      return;
    }
    const placeId = normalizeAxelPlaceId(
      decodeURIComponent(
        url.split("/axel/location/place/")[1]?.split("?")[0] ?? "",
      ),
    );
    if (!placeId) {
      return;
    }
    this.placeNetworkLog.push({
      placeId,
      status,
      url,
      at: Date.now(),
    });
    // #region agent log
    debugAxelLog("H4", "axel-selection-bridge.ts:recordPlaceNetwork", "place-request", {
      placeId,
      status,
      urlTail: url.slice(-80),
    });
    // #endregion
  }

  private async handleResponse(response: Response): Promise<void> {
    const url = response.url();

    if (url.includes(AXEL_PLACE_PREFIX) && !url.includes("reverse-geocode")) {
      this.recordPlaceNetwork(url, response.status());
    }

    if (response.status() !== 200) {
      return;
    }

    if (url.includes(AXEL_PLACE_PREFIX) && !url.includes("reverse-geocode")) {
      const placeId = normalizeAxelPlaceId(
        decodeURIComponent(
          url.split("/axel/location/place/")[1]?.split("?")[0] ?? "",
        ),
      );
      if (placeId) {
        this.committedPlaceIds.add(placeId);
      }
      return;
    }

    if (!url.includes(AXEL_SEARCH_URL) || response.request().method() !== "POST") {
      return;
    }

    try {
      const body = (await response.json()) as SearchResponseBody;
      const before = this.candidates.length;
      this.ingestSearchResults(body);
      const added = this.candidates.length - before;
      if (added > 0) {
        this.searchIngestCount += 1;
        // #region agent log
        debugAxelLog("H1", "axel-selection-bridge.ts:handleResponse", "search-ingested", {
          added,
          totalCandidates: this.candidates.length,
          placeIds: this.candidates.slice(-added).map((c) => c.placeId.slice(0, 20)),
        });
        // #endregion
      }
    } catch {
      // 忽略非 JSON 响应
    }
  }
}
