import { spawn } from "node:child_process";
import { mkdir, rm } from "node:fs/promises";
import { createWriteStream } from "node:fs";
import process from "node:process";
import { setTimeout as delay } from "node:timers/promises";

const chromePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
const port = Number(process.env.CDP_PORT || 9230);
const navigationUrl = process.env.JSX_URL || "https://www.jsx.com/";
const userDataDir = process.env.CHROME_USER_DATA_DIR || `/tmp/jsx-live-monitor-${Date.now()}`;
const artifactsDir = process.env.JSX_ARTIFACTS_DIR || "/tmp/jsx-live-monitor";
const logPath = `${artifactsDir}/network-events.ndjson`;
const requestTimeoutMs = Number(process.env.REQUEST_TIMEOUT_MS || 30000);

await mkdir(artifactsDir, { recursive: true });

const logStream = createWriteStream(logPath, { flags: "a" });

function writeLine(line) {
  console.log(line);
  logStream.write(`${line}\n`);
}

function log(message, details) {
  if (details === undefined) {
    writeLine(`[monitor] ${message}`);
    return;
  }
  writeLine(`[monitor] ${message} ${JSON.stringify(details)}`);
}

async function fetchJson(url, init = undefined) {
  const response = await fetch(url, init);
  if (!response.ok) {
    throw new Error(`HTTP ${response.status} for ${url}`);
  }
  return response.json();
}

async function waitForDebugger() {
  const deadline = Date.now() + requestTimeoutMs;
  while (Date.now() < deadline) {
    try {
      return await fetchJson(`http://127.0.0.1:${port}/json/version`);
    } catch {
      await delay(250);
    }
  }
  throw new Error("Timed out waiting for Chrome remote debugger");
}

async function waitForPageTarget() {
  const deadline = Date.now() + requestTimeoutMs;
  while (Date.now() < deadline) {
    try {
      const targets = await fetchJson(`http://127.0.0.1:${port}/json/list`);
      const pageTarget = targets.find(target =>
        target.type === "page" &&
        typeof target.url === "string" &&
        target.url.startsWith("http") &&
        target.webSocketDebuggerUrl
      );
      if (pageTarget) {
        return pageTarget;
      }
    } catch {
      // Ignore transient startup failures while Chrome is coming up.
    }
    await delay(250);
  }
  throw new Error("Timed out waiting for initial Chrome page target");
}

function launchChrome() {
  const args = [
    `--remote-debugging-port=${port}`,
    `--user-data-dir=${userDataDir}`,
    "--no-first-run",
    "--no-default-browser-check",
    "--disable-default-apps",
    "--disable-popup-blocking",
    "--window-size=1440,1200",
    navigationUrl,
  ];

  log("Launching Chrome", { chromePath, port, userDataDir, navigationUrl, logPath });

  const child = spawn(chromePath, args, {
    detached: false,
    stdio: ["ignore", "pipe", "pipe"],
  });

  child.stdout.on("data", chunk => {
    const text = chunk.toString().trim();
    if (text) {
      writeLine(`[chrome] ${text}`);
    }
  });

  child.stderr.on("data", chunk => {
    const text = chunk.toString().trim();
    if (text) {
      writeLine(`[chrome] ${text}`);
    }
  });

  return child;
}

class CDPClient {
  constructor(socketUrl) {
    this.socketUrl = socketUrl;
    this.ws = new WebSocket(socketUrl);
    this.nextId = 1;
    this.pending = new Map();
    this.eventHandlers = new Map();
    this.openPromise = new Promise((resolve, reject) => {
      this.ws.addEventListener("open", () => resolve());
      this.ws.addEventListener("error", event => reject(event.error || new Error("WebSocket open failed")));
    });

    this.ws.addEventListener("message", event => {
      const message = JSON.parse(event.data.toString());
      if (message.id) {
        const pending = this.pending.get(message.id);
        if (!pending) {
          return;
        }
        this.pending.delete(message.id);
        if (message.error) {
          pending.reject(new Error(message.error.message || JSON.stringify(message.error)));
        } else {
          pending.resolve(message.result);
        }
        return;
      }

      const handlers = this.eventHandlers.get(message.method) || [];
      for (const handler of handlers) {
        handler(message.params || {});
      }
    });
  }

  async ready() {
    await this.openPromise;
  }

  async send(method, params = {}) {
    await this.ready();
    const id = this.nextId++;
    const payload = JSON.stringify({ id, method, params });
    const promise = new Promise((resolve, reject) => {
      this.pending.set(id, { resolve, reject });
      setTimeout(() => {
        if (this.pending.has(id)) {
          this.pending.delete(id);
          reject(new Error(`Timed out waiting for ${method}`));
        }
      }, requestTimeoutMs);
    });

    this.ws.send(payload);
    return promise;
  }

  on(method, handler) {
    const handlers = this.eventHandlers.get(method) || [];
    handlers.push(handler);
    this.eventHandlers.set(method, handlers);
  }

  async close() {
    if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
      this.ws.close();
    }
  }
}

let shuttingDown = false;

async function main() {
  const chrome = launchChrome();
  let cdp;

  const shutdown = async signal => {
    if (shuttingDown) {
      return;
    }
    shuttingDown = true;
    log("Shutting down", { signal });
    if (cdp) {
      await cdp.close();
    }
    chrome.kill("SIGTERM");
    await delay(500);
    await rm(userDataDir, { recursive: true, force: true });
    logStream.end();
    process.exit(0);
  };

  process.on("SIGINT", shutdown);
  process.on("SIGTERM", shutdown);

  try {
    await waitForDebugger();
    const target = await waitForPageTarget();
    cdp = new CDPClient(target.webSocketDebuggerUrl);
    await cdp.ready();

    log("Attached to page target", { id: target.id, url: target.url });
    log("Manual monitor ready", { instructions: "Drive the JSX page in Chrome. All api.jsx.com traffic will stream here." });

    const responseMetaByRequestId = new Map();

    cdp.on("Page.frameNavigated", params => {
      const url = params.frame?.url;
      if (url) {
        log("Page navigated", { url });
      }
    });

    cdp.on("Network.requestWillBeSent", params => {
      const url = params.request?.url || "";
      if (!url.includes("api.jsx.com")) {
        return;
      }
      const record = {
        stage: "request",
        type: params.type || "",
        method: params.request?.method || "",
        url,
        postData: params.request?.postData || "",
        hasAuthHeader: Boolean(params.request?.headers?.Authorization),
      };
      writeLine(JSON.stringify(record));
    });

    cdp.on("Network.responseReceived", params => {
      const url = params.response?.url || "";
      if (!url.includes("api.jsx.com")) {
        return;
      }
      const record = {
        stage: "response",
        type: params.type || "",
        status: params.response?.status || 0,
        mimeType: params.response?.mimeType || "",
        url,
      };
      responseMetaByRequestId.set(params.requestId, record);
      writeLine(JSON.stringify(record));
    });

    cdp.on("Network.loadingFinished", async params => {
      const meta = responseMetaByRequestId.get(params.requestId);
      if (!meta || !meta.url.includes("api.jsx.com")) {
        return;
      }
      try {
        const body = await cdp.send("Network.getResponseBody", { requestId: params.requestId });
        writeLine(JSON.stringify({
          stage: "body",
          status: meta.status,
          url: meta.url,
          bodySnippet: (body.body || "").slice(0, 4000),
          base64Encoded: Boolean(body.base64Encoded),
        }));
      } catch (error) {
        writeLine(JSON.stringify({
          stage: "body-error",
          url: meta.url,
          error: error.message,
        }));
      }
    });

    await cdp.send("Page.enable");
    await cdp.send("Runtime.enable");
    await cdp.send("Network.enable", {
      maxTotalBufferSize: 100_000_000,
      maxResourceBufferSize: 10_000_000,
      maxPostDataSize: 1_000_000,
    });
    await cdp.send("Network.setCacheDisabled", { cacheDisabled: true });

    await new Promise(() => {});
  } catch (error) {
    log("Monitor failed", { error: error.message });
    await shutdown("error");
  }
}

await main();
