import fs from "node:fs";

export type NetworkEvent = {
  ts: number;
  iso: string;
  phase: string;
  kind: "request" | "response";
  method: string;
  url: string;
  status?: number;
  isPlaceCommit?: boolean;
  isLocationSearch?: boolean;
  isQuote?: boolean;
};

export type SearchPlaceChain = {
  index: number;
  phase: string;
  searchRequestTs: number;
  searchResponseTs: number | null;
  searchStatus: number | null;
  placeRequestTs: number | null;
  placeResponseTs: number | null;
  placeStatus: number | null;
  placeId: string | null;
  gapSearchResponseToPlaceRequestMs: number | null;
  gapPlaceRequestToPlaceResponseMs: number | null;
  complete: boolean;
};

export type NetworkSummary = {
  source: string;
  eventCount: number;
  searchResponses200: number;
  placeGet200: number;
  quotePost200: number;
  chains: SearchPlaceChain[];
};

const PLACE_ID_RE = /\/axel\/location\/place\/(ChIJ[^/?]+)/;

export function loadNetworkNdjson(filePath: string): NetworkEvent[] {
  const raw = fs.readFileSync(filePath, "utf8");
  return raw
    .split(/\r?\n/)
    .map((line) => line.trim())
    .filter(Boolean)
    .map((line) => JSON.parse(line) as NetworkEvent);
}

export function extractPlaceId(url: string): string | null {
  const m = url.match(PLACE_ID_RE);
  return m?.[1] ?? null;
}

/** 从 network.ndjson 提取每次 search 200 后是否紧跟 GET place 200 */
export function summarizeNetwork(
  events: NetworkEvent[],
  source: string,
): NetworkSummary {
  const chains: SearchPlaceChain[] = [];
  let searchResponses200 = 0;
  let placeGet200 = 0;
  let quotePost200 = 0;

  for (let i = 0; i < events.length; i += 1) {
    const ev = events[i]!;
    if (
      ev.kind === "response" &&
      ev.isLocationSearch &&
      ev.status === 200 &&
      ev.method.toUpperCase() === "POST"
    ) {
      searchResponses200 += 1;
      const chain: SearchPlaceChain = {
        index: chains.length,
        phase: ev.phase,
        searchRequestTs: findRequestTs(events, i, ev.url, "POST") ?? ev.ts,
        searchResponseTs: ev.ts,
        searchStatus: 200,
        placeRequestTs: null,
        placeResponseTs: null,
        placeStatus: null,
        placeId: null,
        gapSearchResponseToPlaceRequestMs: null,
        gapPlaceRequestToPlaceResponseMs: null,
        complete: false,
      };

      for (let j = i + 1; j < events.length; j += 1) {
        const next = events[j]!;
        if (
          next.kind === "response" &&
          next.isLocationSearch &&
          next.method.toUpperCase() === "POST"
        ) {
          break;
        }
        if (
          next.kind === "request" &&
          next.isPlaceCommit &&
          next.method.toUpperCase() === "GET" &&
          chain.placeRequestTs === null
        ) {
          chain.placeRequestTs = next.ts;
          chain.placeId = extractPlaceId(next.url);
          if (chain.searchResponseTs) {
            chain.gapSearchResponseToPlaceRequestMs =
              next.ts - chain.searchResponseTs;
          }
        }
        if (
          next.kind === "response" &&
          next.isPlaceCommit &&
          next.method.toUpperCase() === "GET" &&
          chain.placeRequestTs !== null &&
          chain.placeResponseTs === null
        ) {
          chain.placeResponseTs = next.ts;
          chain.placeStatus = next.status ?? null;
          if (chain.placeRequestTs) {
            chain.gapPlaceRequestToPlaceResponseMs =
              next.ts - chain.placeRequestTs;
          }
          chain.complete = next.status === 200;
          break;
        }
      }
      chains.push(chain);
    }

    if (
      ev.kind === "response" &&
      ev.isPlaceCommit &&
      ev.status === 200 &&
      ev.method.toUpperCase() === "GET"
    ) {
      placeGet200 += 1;
    }

    if (
      ev.kind === "response" &&
      ev.isQuote &&
      ev.status === 200 &&
      ev.method.toUpperCase() === "POST"
    ) {
      quotePost200 += 1;
    }
  }

  return {
    source,
    eventCount: events.length,
    searchResponses200,
    placeGet200,
    quotePost200,
    chains,
  };
}

function findRequestTs(
  events: NetworkEvent[],
  responseIndex: number,
  url: string,
  method: string,
): number | null {
  for (let i = responseIndex; i >= 0; i -= 1) {
    const ev = events[i]!;
    if (ev.kind === "request" && ev.url === url && ev.method.toUpperCase() === method) {
      return ev.ts;
    }
  }
  return null;
}

export type NetworkDiffVerdict = {
  humanRunId: string;
  rpaRunId: string;
  human: NetworkSummary;
  rpa: NetworkSummary;
  gaps: string[];
  verdict: "MATCH" | "RPA_MISSING_PLACE" | "RPA_PARTIAL" | "HUMAN_INCOMPLETE";
  recommendations: string[];
};

export function diffHumanRpaNetwork(
  humanEvents: NetworkEvent[],
  rpaEvents: NetworkEvent[],
  humanRunId: string,
  rpaRunId: string,
): NetworkDiffVerdict {
  const human = summarizeNetwork(humanEvents, `human:${humanRunId}`);
  const rpa = summarizeNetwork(rpaEvents, `rpa:${rpaRunId}`);
  const gaps: string[] = [];
  const recommendations: string[] = [];

  if (human.placeGet200 < 2) {
    return {
      humanRunId,
      rpaRunId,
      human,
      rpa,
      gaps: ["真人基准 place GET 200 不足 2 次，无法对比"],
      verdict: "HUMAN_INCOMPLETE",
      recommendations: ["重新执行 record:human-commit-baseline"],
    };
  }

  if (rpa.placeGet200 < human.placeGet200) {
    gaps.push(
      `RPA place GET 200: ${rpa.placeGet200}，真人: ${human.placeGet200}`,
    );
  }

  const humanComplete = human.chains.filter((c) => c.complete).length;
  const rpaComplete = rpa.chains.filter((c) => c.complete).length;
  if (rpaComplete < humanComplete) {
    gaps.push(
      `search→place 完整链: RPA ${rpaComplete}/${human.chains.length}，真人 ${humanComplete}/${human.chains.length}`,
    );
  }

  for (let i = 0; i < human.chains.length; i += 1) {
    const h = human.chains[i]!;
    const r = rpa.chains[i];
    const label = `第${i + 1}次(search phase=${h.phase})`;
    if (!r) {
      gaps.push(`${label}: RPA 无对应 search 链`);
      continue;
    }
    if (h.complete && !r.complete) {
      gaps.push(
        `${label}: 真人有 place 200 (gap ${h.gapSearchResponseToPlaceRequestMs}ms)，RPA 无 place 请求`,
      );
    } else if (h.complete && r.complete && h.gapSearchResponseToPlaceRequestMs && r.gapSearchResponseToPlaceRequestMs) {
      const delta = Math.abs(
        r.gapSearchResponseToPlaceRequestMs - h.gapSearchResponseToPlaceRequestMs,
      );
      if (delta > 3000) {
        gaps.push(
          `${label}: search→place 延迟差 ${delta}ms (人 ${h.gapSearchResponseToPlaceRequestMs}ms / RPA ${r.gapSearchResponseToPlaceRequestMs}ms)`,
        );
      }
    }
  }

  if (human.quotePost200 > 0 && rpa.quotePost200 === 0) {
    gaps.push("真人有 axel/quote 200，RPA 本次未触发 quote（可能地址未 commit）");
  }

  let verdict: NetworkDiffVerdict["verdict"] = "MATCH";
  if (rpa.placeGet200 === 0) {
    verdict = "RPA_MISSING_PLACE";
    recommendations.push(
      "RPA 仅有 search 无 place：点选未触发 widget 内部 handler，对照 trace 复刻鼠标点击联想行",
    );
    recommendations.push("禁止新增 fix-place/CDP 策略；改 mothership-address-adapter 点选目标");
  } else if (rpaComplete < humanComplete) {
    verdict = "RPA_PARTIAL";
    recommendations.push("部分侧 place 成功：对比成功侧与失败侧 adapter 日志 method/events");
  }

  if (gaps.length === 0) {
    recommendations.push("网络链路与真人一致，若仍 assert 失败则查 DOM 双地址态非网络层");
  }

  return {
    humanRunId,
    rpaRunId,
    human,
    rpa,
    gaps,
    verdict,
    recommendations,
  };
}
