import { appendFileSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { gzipSync } from "node:zlib";

const webuiRoot = new URL("..", import.meta.url);
const distRoot = new URL("./dist/", webuiRoot);
const manifestPath = new URL("./.vite/manifest.json", distRoot);
const budgetPath = new URL("./bundle-budget.json", webuiRoot);
const metricsPath = new URL("./bundle-metrics.json", distRoot);
const buildName = process.env.GOVOPLAN_WEBUI_BUILD_NAME || "default";

const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
const budgets = JSON.parse(readFileSync(budgetPath, "utf8"));
const entries = Object.entries(manifest);
const entryKeys = entries
  .filter(([, chunk]) => chunk.isEntry && isJavaScript(chunk.file))
  .map(([key]) => key);

if (entryKeys.length === 0) {
  throw new Error("Vite manifest has no JavaScript entry");
}

const initialKeys = new Set();
for (const entryKey of entryKeys) collectStaticImports(entryKey, initialKeys);
const initialFiles = uniqueChunks(
  [...initialKeys].map((key) => [key, manifest[key]]).filter(([, chunk]) => chunk)
);
const initialFileNames = new Set(initialFiles.map(([, chunk]) => chunk.file));
const moduleDescriptorChunks = entries.filter(([key, chunk]) => (
  isJavaScript(chunk.file) &&
  chunk.isDynamicEntry &&
  (
    key.startsWith("virtual:govoplan-installed-module/") ||
    chunk.name?.startsWith("_40govoplan_2F")
  )
));
const eagerModuleDescriptors = moduleDescriptorChunks.filter(
  ([, chunk]) => initialFileNames.has(chunk.file)
);
const asyncFiles = uniqueChunks(
  entries.filter(([, chunk]) => (
    isJavaScript(chunk.file) && !initialFileNames.has(chunk.file)
  ))
);

const initial = summarizeChunks(initialFiles);
const measuredAsyncChunks = asyncFiles.map(measureChunk);
const largestAsync = measuredAsyncChunks.sort(
  (left, right) => right.rawBytes - left.rawBytes
)[0] ?? null;
const failures = [
  ...eagerModuleDescriptors.map(([, chunk]) => (
    `Installed module descriptor ${chunk.file} is part of the initial static closure`
  )),
  ...budgetFailures("Initial JavaScript", initial, budgets.initialJs),
  ...(largestAsync
    ? budgetFailures("Largest async chunk", largestAsync, budgets.asyncChunk)
    : ["No asynchronous JavaScript chunk was produced"])
];

const metrics = {
  schemaVersion: 1,
  buildName,
  measuredAt: new Date().toISOString(),
  initial,
  largestAsync,
  moduleDescriptorCount: moduleDescriptorChunks.length,
  eagerModuleDescriptorCount: eagerModuleDescriptors.length,
  budgets,
  passed: failures.length === 0,
  failures
};
writeFileSync(metricsPath, `${JSON.stringify(metrics, null, 2)}\n`);

console.log(`\nWebUI bundle budget (${buildName})`);
console.log(formatMetric("Initial JavaScript", initial, budgets.initialJs));
console.log(formatMetric("Largest async chunk", largestAsync, budgets.asyncChunk));
console.log(`Metrics: ${metricsPath.pathname}`);
appendCiSummary(metrics);

if (failures.length) {
  for (const failure of failures) console.error(`Bundle budget exceeded: ${failure}`);
  process.exitCode = 1;
}

function collectStaticImports(key, target) {
  if (target.has(key)) return;
  const chunk = manifest[key];
  if (!chunk || !isJavaScript(chunk.file)) return;
  target.add(key);
  for (const importedKey of chunk.imports ?? []) {
    collectStaticImports(importedKey, target);
  }
}

function uniqueChunks(chunks) {
  const byFile = new Map();
  for (const [key, chunk] of chunks) {
    if (!chunk || byFile.has(chunk.file)) continue;
    byFile.set(chunk.file, [key, chunk]);
  }
  return [...byFile.values()];
}

function summarizeChunks(chunks) {
  const measured = chunks.map(measureChunk);
  return {
    assetCount: measured.length,
    rawBytes: measured.reduce((total, chunk) => total + chunk.rawBytes, 0),
    gzipBytes: measured.reduce((total, chunk) => total + chunk.gzipBytes, 0),
    assets: measured
  };
}

function measureChunk([key, chunk]) {
  const assetUrl = new URL(chunk.file, distRoot);
  const source = readFileSync(assetUrl);
  return {
    key,
    file: chunk.file,
    rawBytes: statSync(assetUrl).size,
    gzipBytes: gzipSync(source, { level: 9 }).length
  };
}

function budgetFailures(label, measured, budget) {
  if (!measured) return [`${label} was not measured`];
  const failures = [];
  for (const field of ["rawBytes", "gzipBytes"]) {
    if (measured[field] > budget[field]) {
      failures.push(
        `${label} ${field} is ${measured[field]} bytes; budget is ${budget[field]} bytes`
      );
    }
  }
  return failures;
}

function formatMetric(label, measured, budget) {
  if (!measured) return `${label}: not produced`;
  const file = measured.file ? ` (${measured.file})` : ` (${measured.assetCount} assets)`;
  return [
    `${label}${file}:`,
    `${formatBytes(measured.rawBytes)} raw / ${formatBytes(budget.rawBytes)} budget,`,
    `${formatBytes(measured.gzipBytes)} gzip / ${formatBytes(budget.gzipBytes)} budget`
  ].join(" ");
}

function formatBytes(value) {
  return `${(value / 1024).toFixed(1)} KiB`;
}

function isJavaScript(file) {
  return typeof file === "string" && file.endsWith(".js");
}

function appendCiSummary(result) {
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
  if (!summaryPath) return;
  const status = result.passed ? "pass" : "fail";
  const asyncFile = result.largestAsync?.file ?? "none";
  appendFileSync(
    summaryPath,
    [
      "",
      `### WebUI bundle budget: ${result.buildName}`,
      "",
      "| Status | Initial raw | Initial gzip | Largest async | Async raw | Async gzip |",
      "| --- | ---: | ---: | --- | ---: | ---: |",
      `| ${status} | ${formatBytes(result.initial.rawBytes)} | ${formatBytes(result.initial.gzipBytes)} | \`${asyncFile}\` | ${formatBytes(result.largestAsync?.rawBytes ?? 0)} | ${formatBytes(result.largestAsync?.gzipBytes ?? 0)} |`,
      ""
    ].join("\n")
  );
}
