import fs from "node:fs";
import type { Page } from "playwright";

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

export type CommitMoment = {
  ts: number;
  iso: string;
  url: string;
  status: number;
  method: string;
  msSinceRecordingStart: number;
  msSinceLastCheckpoint: number | null;
  checkpointPhase: string;
};

export class NetworkLogger {
  private readonly events: NetworkEvent[] = [];

  private readonly placeCommits: CommitMoment[] = [];

  private recordingStartedAt = Date.now();

  private lastCheckpointAt: number | null = null;

  private checkpointPhase = "recording_start";

  constructor(
    private readonly outPath: string,
    private readonly onPlaceCommit?: (moment: CommitMoment) => void,
  ) {
    fs.mkdirSync(pathDir(outPath), { recursive: true });
  }

  setCheckpoint(phase: string): void {
    this.checkpointPhase = phase;
    this.lastCheckpointAt = Date.now();
  }

  attach(page: Page): void {
    page.on("request", (req) => {
      const url = req.url();
      if (!this.isAxelRelevant(url)) {
        return;
      }
      this.append({
        ts: Date.now(),
        iso: new Date().toISOString(),
        phase: this.checkpointPhase,
        kind: "request",
        method: req.method(),
        url: url.slice(0, 500),
        resourceType: req.resourceType(),
        isPlaceCommit: this.isPlaceCommitUrl(url),
        isLocationSearch: url.includes("/axel/location/search"),
        isQuote: url.includes("/axel/quote"),
      });
    });

    page.on("response", async (res) => {
      const url = res.url();
      if (!this.isAxelRelevant(url)) {
        return;
      }
      const status = res.status();
      this.append({
        ts: Date.now(),
        iso: new Date().toISOString(),
        phase: this.checkpointPhase,
        kind: "response",
        method: res.request().method(),
        url: url.slice(0, 500),
        status,
        resourceType: res.request().resourceType(),
        isPlaceCommit: this.isPlaceCommitUrl(url),
        isLocationSearch: url.includes("/axel/location/search"),
        isQuote: url.includes("/axel/quote"),
      });

      if (
        this.isPlaceCommitUrl(url) &&
        status === 200 &&
        res.request().method().toUpperCase() === "GET"
      ) {
        const now = Date.now();
        const moment: CommitMoment = {
          ts: now,
          iso: new Date(now).toISOString(),
          url: url.slice(0, 500),
          status,
          method: "GET",
          msSinceRecordingStart: now - this.recordingStartedAt,
          msSinceLastCheckpoint:
            this.lastCheckpointAt === null ? null : now - this.lastCheckpointAt,
          checkpointPhase: this.checkpointPhase,
        };
        this.placeCommits.push(moment);
        this.onPlaceCommit?.(moment);
        console.log(
          `[录证] ★ place commit ${status} @ +${moment.msSinceRecordingStart}ms phase=${this.checkpointPhase}`,
        );
        console.log(`         ${url.slice(0, 120)}`);
      }
    });
  }

  flush(): void {
    fs.writeFileSync(this.outPath, this.events.map((e) => JSON.stringify(e)).join("\n") + "\n", "utf8");
  }

  getPlaceCommits(): CommitMoment[] {
    return [...this.placeCommits];
  }

  private isAxelRelevant(url: string): boolean {
    return (
      url.includes("services.mothership.com/axel/location") ||
      url.includes("services.mothership.com/axel/quote") ||
      url.includes("maps.googleapis.com") ||
      url.includes("places.googleapis.com")
    );
  }

  private isPlaceCommitUrl(url: string): boolean {
    return (
      url.includes("/axel/location/place/") &&
      !url.includes("reverse-geocode")
    );
  }

  private append(event: NetworkEvent): void {
    this.events.push(event);
    fs.appendFileSync(this.outPath, `${JSON.stringify(event)}\n`, "utf8");
  }
}

function pathDir(filePath: string): string {
  const idx = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\"));
  return idx >= 0 ? filePath.slice(0, idx) : filePath;
}
