import type { Page, Response } from "playwright";
import {
  formatPinnedTelemetry,
  isJsonNetworkCandidate,
  RPA_QUOTE_CAPTURE_TIMEOUT_MS,
  RPA_QUOTE_POST_SUBMIT_SETTLE_MS,
} from "@/workers/rpa/quote-capture/constants";
import { resolveRpaQuoteCaptureMs } from "@/lib/rpa/env";
import { isAxelQuoteResponseUrl } from "@/workers/rpa/quote-capture/axel-contract";
import { normalizeQuote } from "@/workers/rpa/quote-capture/quote-normalize";
import {
  attachNetworkVisibility,
  type NetworkTraceEntry,
} from "@/workers/rpa/quote-capture/quote-network-visibility";
import type { QuoteCaptureSignals } from "@/workers/rpa/quote-capture/types";

async function tryParseResponseJson(
  response: Response,
): Promise<unknown | null> {
  try {
    return await response.json();
  } catch {
    return null;
  }
}

async function pollUntilAxelQuote(
  getAxelCount: () => number,
  deadlineMs: number,
): Promise<void> {
  while (Date.now() < deadlineMs) {
    if (getAxelCount() > 0) {
      return;
    }
    await new Promise((resolve) => setTimeout(resolve, 200));
  }
}

function countAxelPayloads(
  contractPayloads: QuoteCaptureSignals["contractPayloads"],
  jsonPayloads: QuoteCaptureSignals["jsonPayloads"],
): number {
  const contractAxel = contractPayloads.filter((p) =>
    isAxelQuoteResponseUrl(p.url),
  ).length;
  const jsonAxel = jsonPayloads.filter((p) => isAxelQuoteResponseUrl(p.url)).length;
  return contractAxel + jsonAxel;
}

function isQuoteRateApiResponse(response: Response): boolean {
  if (response.status() < 200 || response.status() >= 300) {
    return false;
  }
  const url = response.url().toLowerCase();
  if (url.includes("axel/quote") || url.includes("services.mothership.com/axel")) {
    return true;
  }
  if (
    /dashboard\.mothership\.com|api\.mothership\.com|plugins\.mothership\.com/.test(
      url,
    ) &&
    /quote|rate|pricing|graphql/.test(url)
  ) {
    return true;
  }
  return false;
}

async function drainInflight(inflight: Set<Promise<void>>): Promise<void> {
  if (inflight.size === 0) {
    return;
  }
  await Promise.allSettled([...inflight]);
}

async function waitForSignUpQuoteRates(page: Page, timeoutMs: number): Promise<void> {
  if (!page.url().includes("sign-up-quote")) {
    return;
  }
  console.log("[rpa] quote-network: sign-up-quote 页等待 widget 拉价…");
  const deadline = Date.now() + timeoutMs;
  const remaining = () => Math.max(500, deadline - Date.now());

  /** 先等 quoter-sign-up.js，再 axel/quote；勿用 networkidle（会吞掉整段超时且 axel 尚未发起） */
  await page
    .waitForResponse(
      (response) =>
        response.url().includes("quoter-sign-up.js") &&
        response.status() >= 200 &&
        response.status() < 300,
      { timeout: remaining() },
    )
    .catch(() => undefined);

  await page
    .waitForResponse((response) => isQuoteRateApiResponse(response), {
      timeout: remaining(),
    })
    .catch(() => undefined);

  await page.waitForTimeout(Math.min(1_500, remaining()));
}

/**
 * 契约驱动捕获：listener 在 submit 之前挂载；drain 异步 handler 避免 json=0 竞态。
 */
export async function captureQuoteNetworkResponses(
  page: Page,
  submit: () => Promise<void>,
): Promise<QuoteCaptureSignals> {
  const contractPayloads: QuoteCaptureSignals["contractPayloads"] = [];
  const jsonPayloads: QuoteCaptureSignals["jsonPayloads"] = [];
  const seenKeys = new Set<string>();
  const sniffedUrls: string[] = [];
  const inflight = new Set<Promise<void>>();

  const visibility = attachNetworkVisibility(page);
  let networkTrace: NetworkTraceEntry[] = [];

  const onResponse = (response: Response): void => {
    const task = (async () => {
      try {
        if (response.status() < 200 || response.status() >= 300) {
          return;
        }
        const url = response.url();
        const contentType = response.headers()["content-type"] ?? "";

        if (!isJsonNetworkCandidate(url, contentType)) {
          return;
        }

        if (process.env.RPA_QUOTE_NETWORK_SNIFF === "true") {
          sniffedUrls.push(url);
        }

        const body = await tryParseResponseJson(response);
        if (body === null) {
          return;
        }

        jsonPayloads.push({ url, body });

        if (!normalizeQuote(body)) {
          return;
        }

        const key = url;
        if (seenKeys.has(key)) {
          return;
        }
        seenKeys.add(key);

        contractPayloads.push({ url, body });
        console.log(
          `[rpa] quote-contract match url=${url} ${formatPinnedTelemetry(url)}`,
        );
      } catch {
        /* 忽略非 JSON 响应 */
      }
    })();
    inflight.add(task);
    void task.finally(() => {
      inflight.delete(task);
    });
  };

  page.on("response", onResponse);

  const captureTimeoutMs = resolveRpaQuoteCaptureMs(RPA_QUOTE_CAPTURE_TIMEOUT_MS);

  await submit();
  console.log(`[rpa] quote-network: post-submit url=${page.url()}`);

  /** deadline 须在 submit 之后，否则长地址步会耗尽 capture 窗口 */
  const deadline = Date.now() + captureTimeoutMs;
  const remaining = () => Math.max(500, deadline - Date.now());
  const axelCount = () =>
    countAxelPayloads(contractPayloads, jsonPayloads);

  await waitForSignUpQuoteRates(page, captureTimeoutMs);

  if (axelCount() === 0) {
    const { isInlineQuotePanelVisible } = await import(
      "@/workers/rpa/quote-capture/post-submit-navigation"
    );
    const inlineDeadline = Date.now() + Math.min(15_000, remaining());
    while (Date.now() < inlineDeadline && axelCount() === 0) {
      if (await isInlineQuotePanelVisible(page)) {
        console.log("[rpa] quote-network: 首页内嵌报价已出现，等待 axel/quote…");
        break;
      }
      await page.waitForTimeout(200);
    }
  }

  await pollUntilAxelQuote(axelCount, deadline);

  if (axelCount() === 0 && page.url().includes("sign-up-quote")) {
    console.log("[rpa] quote-network: sign-up-quote 补等 axel/quote…");
    await page
      .waitForResponse((response) => isQuoteRateApiResponse(response), {
        timeout: remaining(),
      })
      .catch(() => null);
    await drainInflight(inflight);
    await pollUntilAxelQuote(axelCount, Date.now() + Math.min(8_000, remaining()));
  }

  await page.waitForTimeout(
    Math.min(RPA_QUOTE_POST_SUBMIT_SETTLE_MS, remaining()),
  );

  page.off("response", onResponse);
  await drainInflight(inflight);

  networkTrace = [...visibility.trace];
  visibility.logSummary("capture-done");
  visibility.detach();

  if (sniffedUrls.length > 0) {
    console.log(
      `[rpa] quote-network-sniff (telemetry): ${[...new Set(sniffedUrls)].join(", ")}`,
    );
  }

  return {
    contractPayloads,
    jsonPayloads,
    networkTrace,
    resultUrl: page.url(),
  };
}

/** @deprecated 使用 captureQuoteNetworkResponses */
export const captureQuoteEvents = captureQuoteNetworkResponses;
