import { chromium, type BrowserContext, type Request } from "playwright";
import { existsSync } from "node:fs";
import { resolve } from "node:path";
import { readConfig } from "./config";

interface CodeQueuePerfOptions {
  url: string;
  username: string;
  password: string;
  timeoutMs: number;
  targetMs: number;
  headless: boolean;
  json: boolean;
}

interface ApiTiming {
  url: string;
  method: string;
  status: number;
  durationMs: number;
}

function argValue(args: string[], name: string): string | null {
  const index = args.indexOf(name);
  if (index === -1) return null;
  const value = args[index + 1];
  if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
  return value;
}

function numberArg(args: string[], name: string, fallback: number): number {
  const raw = argValue(args, name);
  if (raw === null) return fallback;
  const value = Number(raw);
  if (!Number.isFinite(value) || value <= 0) throw new Error(`${name} must be a positive number`);
  return Math.floor(value);
}

function normalizeBaseUrl(value: string): string {
  return value.replace(/\/+$/u, "");
}

function readOptions(): CodeQueuePerfOptions {
  const config = readConfig();
  const args = process.argv.slice(2);
  return {
    url: normalizeBaseUrl(argValue(args, "--url") ?? `http://${config.network.publicHost}:${config.network.frontend.port}`),
    username: argValue(args, "--username") ?? config.auth.username,
    password: argValue(args, "--password") ?? config.auth.password,
    timeoutMs: numberArg(args, "--timeout-ms", 90_000),
    targetMs: numberArg(args, "--target-ms", 1_000),
    headless: !args.includes("--headed"),
    json: args.includes("--json"),
  };
}

async function authenticateSession(context: BrowserContext, options: CodeQueuePerfOptions): Promise<void> {
  const response = await fetch(`${options.url}/login`, {
    method: "POST",
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ username: options.username, password: options.password }),
    signal: AbortSignal.timeout(options.timeoutMs),
  });
  if (!response.ok) {
    throw new Error(`login failed: HTTP ${response.status} ${await response.text()}`);
  }
  const setCookie = response.headers.get("set-cookie");
  const cookiePair = setCookie?.split(";", 1)[0] ?? "";
  const separator = cookiePair.indexOf("=");
  if (separator <= 0) throw new Error("login response did not set a session cookie");
  await context.addCookies([{
    name: cookiePair.slice(0, separator),
    value: cookiePair.slice(separator + 1),
    url: options.url,
  }]);
}

function requestPath(request: Request): string {
  try {
    const url = new URL(request.url());
    return `${url.pathname}${url.search}`;
  } catch {
    return request.url();
  }
}

function parseNumberAttr(value: string | null): number | null {
  const parsed = Number(value);
  return Number.isFinite(parsed) ? parsed : null;
}

async function runCodeQueuePerf(options: CodeQueuePerfOptions): Promise<Record<string, unknown>> {
  const browsersPath = process.env.PLAYWRIGHT_BROWSERS_PATH || ".state/playwright-browsers";
  const fullChromePath = resolve(browsersPath, "chromium-1217/chrome-linux64/chrome");
  const launchArgs = [
    "--no-sandbox",
    "--disable-setuid-sandbox",
    "--disable-dev-shm-usage",
  ];
  const launchOptions: Parameters<typeof chromium.launch>[0] = { headless: options.headless, args: launchArgs };
  if (existsSync(fullChromePath)) {
    launchOptions.executablePath = fullChromePath;
  }
  const browser = await chromium.launch(launchOptions);
  const apiStartedAt = new Map<Request, number>();
  const apiTimings: ApiTiming[] = [];
  const consoleErrors: string[] = [];
  const targetUrl = `${options.url}/app/code-queue/`;
  const measuredAt = new Date().toISOString();

  try {
    const context = await browser.newContext();
    await authenticateSession(context, options);
    const page = await context.newPage();
    await page.route("**/favicon.ico", (route) => route.abort());
    page.on("console", (message) => {
      if (message.type() === "error") consoleErrors.push(message.text().slice(0, 500));
    });
    page.on("request", (request) => {
      const path = requestPath(request);
      if (path.startsWith("/api/") || path === "/app.js") apiStartedAt.set(request, performance.now());
    });
    page.on("response", (response) => {
      const request = response.request();
      const started = apiStartedAt.get(request);
      if (started === undefined) return;
      apiStartedAt.delete(request);
      apiTimings.push({
        url: requestPath(request),
        method: request.method(),
        status: response.status(),
        durationMs: Math.round((performance.now() - started) * 10) / 10,
      });
    });

    const startedAt = performance.now();
    await page.goto(targetUrl, { waitUntil: "domcontentloaded", timeout: options.timeoutMs });
    const domContentLoadedMs = Math.round((performance.now() - startedAt) * 10) / 10;
    await page.waitForSelector('[data-testid="code-queue-page"][data-load-state="complete"]', { timeout: options.timeoutMs });
    const completeAt = performance.now();
    const dom = await page.evaluate(() => {
      const pageElement = document.querySelector('[data-testid="code-queue-page"]') as HTMLElement | null;
      const output = document.querySelector('[data-testid="codex-output"]') as HTMLElement | null;
      const tasks = document.querySelectorAll('[data-testid^="codex-task-codex_"]');
      return {
        loadState: pageElement?.dataset.loadState ?? "",
        componentLoadMs: pageElement?.dataset.loadTotalMs ?? "",
        queueMs: pageElement?.dataset.loadQueueMs ?? "",
        detailMs: pageElement?.dataset.loadDetailMs ?? "",
        transcriptRows: pageElement?.dataset.loadTranscriptRows ?? "",
        taskId: pageElement?.dataset.loadTaskId ?? "",
        partial: pageElement?.dataset.loadPartial === "true",
        visibleTaskCount: tasks.length,
        outputChars: output?.textContent?.length ?? 0,
      };
    });
    let networkIdleReached = true;
    try {
      await page.waitForLoadState("networkidle", { timeout: 5_000 });
    } catch {
      networkIdleReached = false;
    }
    const networkIdleMs = Math.round((performance.now() - startedAt) * 10) / 10;
    const playwrightObservedMs = Math.round((completeAt - startedAt) * 10) / 10;
    const browserLoadMs = parseNumberAttr(dom.componentLoadMs);
    const totalLoadMs = browserLoadMs ?? playwrightObservedMs;

    const slowestApi = apiTimings.slice().sort((left, right) => right.durationMs - left.durationMs).slice(0, 8);
    const result = {
      ok: true,
      measuredAt,
      url: targetUrl,
      targetMs: options.targetMs,
      withinTarget: totalLoadMs <= options.targetMs,
      status: totalLoadMs <= options.targetMs ? "passed" : "slow",
      wallMs: totalLoadMs,
      totalLoadMs,
      domContentLoadedMs,
      appCompleteMs: playwrightObservedMs,
      playwrightObservedMs,
      networkIdleMs,
      networkIdleReached,
      componentLoadMs: browserLoadMs,
      queueMs: parseNumberAttr(dom.queueMs),
      detailMs: parseNumberAttr(dom.detailMs),
      transcriptRows: parseNumberAttr(dom.transcriptRows),
      partial: dom.partial,
      selectedTaskId: dom.taskId || null,
      visibleTaskCount: dom.visibleTaskCount,
      outputChars: dom.outputChars,
      apiRequestCount: apiTimings.length,
      slowestApi,
      consoleErrors,
    };
    await context.close();
    return result;
  } catch (error) {
    return {
      ok: false,
      measuredAt,
      url: targetUrl,
      targetMs: options.targetMs,
      status: "failed",
      error: error instanceof Error ? error.message : String(error),
      apiRequestCount: apiTimings.length,
      slowestApi: apiTimings.slice().sort((left, right) => right.durationMs - left.durationMs).slice(0, 8),
      consoleErrors,
    };
  } finally {
    await browser.close();
  }
}

const options = readOptions();
const result = await runCodeQueuePerf(options);
if (options.json) {
  console.log(JSON.stringify(result));
} else {
  console.log(JSON.stringify(result, null, 2));
}
if (result.ok !== true) process.exitCode = 1;
