#!/usr/bin/env node

import { readFile, stat } from "node:fs/promises";
import path from "node:path";

const rootDir = process.cwd();
const manifestPath = path.resolve(rootDir, process.argv[2] || "apps/frontend/.vinxi/build/client/_build/.vite/manifest.json");
const assetsDir = path.resolve(path.dirname(path.dirname(manifestPath)), "assets");

const budget = {
  maxTotalJsBytes: Number.parseInt(process.env.MAX_TOTAL_JS_BYTES || "320000", 10),
  maxTotalCssBytes: Number.parseInt(process.env.MAX_TOTAL_CSS_BYTES || "50000", 10),
  maxEntryJsBytes: Number.parseInt(process.env.MAX_ENTRY_JS_BYTES || "35000", 10),
  maxRouteJsBytes: Number.parseInt(process.env.MAX_ROUTE_JS_BYTES || "45000", 10),
  maxChunkJsBytes: Number.parseInt(process.env.MAX_CHUNK_JS_BYTES || "50000", 10)
};

function formatBytes(bytes) {
  if (bytes < 1024) {
    return `${bytes} B`;
  }
  if (bytes < 1024 * 1024) {
    return `${(bytes / 1024).toFixed(1)} KB`;
  }
  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
}

async function readManifest(filePath) {
  const raw = await readFile(filePath, "utf8");
  return JSON.parse(raw);
}

function parseRouteEntries(manifest) {
  return Object.entries(manifest)
    .filter(([key]) => key.startsWith("src/routes/") && key.includes("?pick=default&pick=$css"))
    .map(([key, value]) => ({
      routeKey: key,
      file: value?.file
    }))
    .filter(entry => typeof entry.file === "string" && entry.file.endsWith(".js"));
}

async function fileSizeForAsset(assetFile) {
  const absolute = path.resolve(assetsDir, path.basename(assetFile));
  const info = await stat(absolute);
  return info.size;
}

async function main() {
  const failures = [];

  let manifest;
  try {
    manifest = await readManifest(manifestPath);
  } catch (error) {
    console.error(
      `[error] failed to read frontend build manifest at ${manifestPath}: ${error instanceof Error ? error.message : String(error)}`,
    );
    process.exit(1);
  }

  const values = Object.values(manifest).filter(Boolean);
  const jsAssets = new Set();
  const cssAssets = new Set();
  for (const item of values) {
    if (typeof item?.file === "string") {
      if (item.file.endsWith(".js")) {
        jsAssets.add(path.basename(item.file));
      } else if (item.file.endsWith(".css")) {
        cssAssets.add(path.basename(item.file));
      }
    }
    if (Array.isArray(item?.css)) {
      for (const cssFile of item.css) {
        if (typeof cssFile === "string" && cssFile.endsWith(".css")) {
          cssAssets.add(path.basename(cssFile));
        }
      }
    }
  }

  let totalJsBytes = 0;
  let totalCssBytes = 0;
  let largestJsChunk = { name: "", size: 0 };

  for (const jsFile of jsAssets) {
    const size = await fileSizeForAsset(jsFile);
    totalJsBytes += size;
    if (size > largestJsChunk.size) {
      largestJsChunk = { name: jsFile, size };
    }
  }

  for (const cssFile of cssAssets) {
    totalCssBytes += await fileSizeForAsset(cssFile);
  }

  const entryKey = "virtual:$vinxi/handler/client";
  const entryFile = manifest[entryKey]?.file;
  if (!entryFile) {
    failures.push(`missing entry chunk in manifest: ${entryKey}`);
  }

  const entrySize = entryFile ? await fileSizeForAsset(entryFile) : 0;

  const routeEntries = parseRouteEntries(manifest);
  const routeSizes = [];
  for (const route of routeEntries) {
    routeSizes.push({
      ...route,
      size: await fileSizeForAsset(route.file)
    });
  }

  routeSizes.sort((left, right) => right.size - left.size);
  const largestRoute = routeSizes[0] ?? { routeKey: "n/a", file: "n/a", size: 0 };

  if (totalJsBytes > budget.maxTotalJsBytes) {
    failures.push(`total JS size ${formatBytes(totalJsBytes)} exceeds budget ${formatBytes(budget.maxTotalJsBytes)}`);
  }
  if (totalCssBytes > budget.maxTotalCssBytes) {
    failures.push(`total CSS size ${formatBytes(totalCssBytes)} exceeds budget ${formatBytes(budget.maxTotalCssBytes)}`);
  }
  if (entrySize > budget.maxEntryJsBytes) {
    failures.push(`entry chunk ${path.basename(entryFile)} is ${formatBytes(entrySize)} (budget ${formatBytes(budget.maxEntryJsBytes)})`);
  }
  if (largestJsChunk.size > budget.maxChunkJsBytes) {
    failures.push(
      `largest JS chunk ${largestJsChunk.name} is ${formatBytes(largestJsChunk.size)} (budget ${formatBytes(budget.maxChunkJsBytes)})`,
    );
  }
  if (largestRoute.size > budget.maxRouteJsBytes) {
    failures.push(
      `largest route chunk ${path.basename(largestRoute.file)} is ${formatBytes(largestRoute.size)} (budget ${formatBytes(budget.maxRouteJsBytes)})`,
    );
  }

  console.log(`[info] frontend budget report (${manifestPath})`);
  console.log(`[info] total JS: ${formatBytes(totalJsBytes)} (${jsAssets.size} files)`);
  console.log(`[info] total CSS: ${formatBytes(totalCssBytes)} (${cssAssets.size} files)`);
  console.log(`[info] entry JS: ${formatBytes(entrySize)} (${entryFile ? path.basename(entryFile) : "missing"})`);
  console.log(`[info] largest JS chunk: ${formatBytes(largestJsChunk.size)} (${largestJsChunk.name || "none"})`);
  console.log(
    `[info] largest route chunk: ${formatBytes(largestRoute.size)} (${path.basename(largestRoute.file || "none")} / ${largestRoute.routeKey})`,
  );

  if (routeSizes.length) {
    const topRoutes = routeSizes.slice(0, 5);
    console.log("[info] top route chunks:");
    for (const route of topRoutes) {
      console.log(`  - ${route.routeKey}: ${formatBytes(route.size)} (${path.basename(route.file)})`);
    }
  }

  if (failures.length) {
    for (const failure of failures) {
      console.error(`[error] ${failure}`);
    }
    process.exit(1);
  }

  console.log("[ok] frontend bundle budgets passed.");
}

void main();
