import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { spawn } from "node:child_process";
import { setTimeout as sleep } from "node:timers/promises";

import lighthouse from "lighthouse";
import { launch } from "chrome-launcher";

type Theme = "light" | "dark" | "high-contrast";
type Device = "mobile" | "desktop";

type Variant = {
  id: `${Theme}-${Device}`;
  theme: Theme;
  device: Device;
  screenEmulation: {
    mobile: boolean;
    width: number;
    height: number;
    deviceScaleFactor: number;
    disabled: false;
  };
};

type CategoryKey = "performance" | "accessibility" | "best-practices" | "seo";

const VARIANTS: Variant[] = [
  {
    id: "light-mobile",
    theme: "light",
    device: "mobile",
    screenEmulation: { mobile: true, width: 360, height: 800, deviceScaleFactor: 2, disabled: false },
  },
  {
    id: "dark-mobile",
    theme: "dark",
    device: "mobile",
    screenEmulation: { mobile: true, width: 360, height: 800, deviceScaleFactor: 2, disabled: false },
  },
  {
    id: "high-contrast-mobile",
    theme: "high-contrast",
    device: "mobile",
    screenEmulation: { mobile: true, width: 360, height: 800, deviceScaleFactor: 2, disabled: false },
  },
  {
    id: "light-desktop",
    theme: "light",
    device: "desktop",
    screenEmulation: { mobile: false, width: 1350, height: 940, deviceScaleFactor: 1, disabled: false },
  },
  {
    id: "dark-desktop",
    theme: "dark",
    device: "desktop",
    screenEmulation: { mobile: false, width: 1350, height: 940, deviceScaleFactor: 1, disabled: false },
  },
  {
    id: "high-contrast-desktop",
    theme: "high-contrast",
    device: "desktop",
    screenEmulation: { mobile: false, width: 1350, height: 940, deviceScaleFactor: 1, disabled: false },
  },
];

const CATEGORIES: CategoryKey[] = ["performance", "accessibility", "best-practices", "seo"];

function hasFlag(flag: string): boolean {
  return process.argv.includes(flag);
}

function getArg(name: string): string | undefined {
  const prefix = `${name}=`;
  const value = process.argv.find((arg) => arg.startsWith(prefix));
  return value ? value.slice(prefix.length) : undefined;
}

function median(values: number[]): number {
  const sorted = [...values].sort((a, b) => a - b);
  const mid = Math.floor(sorted.length / 2);
  return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
}

async function startPreviewServerIfNeeded(targetUrl: URL): Promise<(() => void) | null> {
  const forceNoPreview = process.env.LIGHTHOUSE_SKIP_PREVIEW === "true";
  if (forceNoPreview) return null;

  const isLocal = targetUrl.hostname === "127.0.0.1" || targetUrl.hostname === "localhost";
  if (!isLocal) return null;

  const port = Number(targetUrl.port || "4321");
  const astroCli = path.join(process.cwd(), "node_modules", "astro", "astro.js");
  const child = spawn(process.execPath, [astroCli, "preview", "--host", "127.0.0.1", "--port", String(port)], {
    cwd: process.cwd(),
    stdio: "pipe",
    env: process.env,
  });

  let ready = false;
  child.stdout.on("data", (buf) => {
    const line = String(buf);
    if (line.includes("Local") || line.includes("localhost") || line.includes("127.0.0.1")) {
      ready = true;
    }
  });

  child.stderr.on("data", () => {});

  for (let i = 0; i < 30; i += 1) {
    if (ready) break;
    await sleep(500);
  }

  await sleep(1500);

  return () => {
    child.kill("SIGTERM");
  };
}

async function run() {
  const outputDir = getArg("--out") || process.env.LIGHTHOUSE_OUTPUT_DIR || "lighthouse-reports";
  const target = getArg("--url") || process.env.LIGHTHOUSE_TARGET_URL || "http://127.0.0.1:4321/";
  const runCount = Number(getArg("--runs") || process.env.LIGHTHOUSE_RUNS || "3");
  const assert100 = hasFlag("--assert-100");

  if (!Number.isInteger(runCount) || runCount < 1) {
    throw new Error(`Invalid run count: ${runCount}`);
  }

  const targetUrl = new URL(target);
  await mkdir(outputDir, { recursive: true });

  const stopPreview = await startPreviewServerIfNeeded(targetUrl);

  try {
    const summary: Record<string, unknown> = {
      target,
      runCount,
      generatedAt: new Date().toISOString(),
      variants: {},
    };

    let gateFailed = false;

    for (const variant of VARIANTS) {
      const byCategory: Record<CategoryKey, number[]> = {
        performance: [],
        accessibility: [],
        "best-practices": [],
        seo: [],
      };

      for (let runIndex = 1; runIndex <= runCount; runIndex += 1) {
        let chrome:
          | Awaited<ReturnType<typeof launch>>
          | undefined;
        for (let attempt = 1; attempt <= 3; attempt += 1) {
          try {
            chrome = await launch({ chromeFlags: ["--headless=new", "--disable-gpu", "--no-sandbox"] });
            break;
          } catch (error) {
            const message = String(error || "");
            if (!message.includes("EPERM") || attempt === 3) {
              throw error;
            }
            await sleep(250 * attempt);
          }
        }

        if (!chrome) {
          throw new Error(`Failed to launch Chrome for ${variant.id} run ${runIndex}.`);
        }

        try {
          let result:
            | Awaited<ReturnType<typeof lighthouse>>
            | undefined;

          for (let attempt = 1; attempt <= 3; attempt += 1) {
            try {
              result = await lighthouse(target, {
                port: chrome.port,
                logLevel: "error",
                output: "json",
                onlyCategories: CATEGORIES,
                formFactor: variant.device,
                screenEmulation: variant.screenEmulation,
                throttlingMethod: "simulate",
                disableStorageReset: true,
                extraHeaders: { Cookie: `site_theme=${encodeURIComponent(variant.theme)}` },
              });
              break;
            } catch (error) {
              const message = String(error || "");
              if (!message.includes("EPERM") || attempt === 3) {
                throw error;
              }
              await sleep(250 * attempt);
            }
          }

          if (!result?.lhr || typeof result.report !== "string") {
            throw new Error(`Lighthouse did not produce a report for ${variant.id} run ${runIndex}.`);
          }

          const lhr = result.lhr;
          const reportPath = path.join(outputDir, `${variant.id}.run-${runIndex}.json`);
          await writeFile(reportPath, result.report, "utf8");

          for (const category of CATEGORIES) {
            const score = lhr.categories?.[category]?.score ?? 0;
            byCategory[category].push(Math.round(score * 100));
          }
        } finally {
          chrome.kill();
        }
      }

      const medians = Object.fromEntries(
        CATEGORIES.map((category) => [category, median(byCategory[category])]),
      ) as Record<CategoryKey, number>;

      if (assert100) {
        for (const category of CATEGORIES) {
          if (medians[category] < 100) {
            gateFailed = true;
          }
        }
      }

      summary.variants = {
        ...(summary.variants as Record<string, unknown>),
        [variant.id]: {
          theme: variant.theme,
          device: variant.device,
          scoresByRun: byCategory,
          medianScores: medians,
        },
      };
    }

    await writeFile(path.join(outputDir, "summary.json"), JSON.stringify(summary, null, 2), "utf8");

    if (assert100 && gateFailed) {
      throw new Error("Lighthouse quality gate failed: one or more variant category medians are below 100.");
    }

    // eslint-disable-next-line no-console
    console.log(`[lighthouse] wrote reports to ${outputDir}`);
  } finally {
    stopPreview?.();
  }
}

run().catch((e) => {
  // eslint-disable-next-line no-console
  console.error(`[lighthouse] ${String(e)}`);
  process.exitCode = 1;
});
