#!/usr/bin/env node

import { execSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { existsSync, readFileSync, writeFileSync, rmSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

const scriptDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptDir, '../..');
const outDir = resolve(repoRoot, '.spa-dist');
const manifestPath = resolve(outDir, '.vite/manifest.json');
const outputFile = resolve(repoRoot, 'src/web/spa-assets.ts');
const DEFAULT_JS_BUDGET_BYTES = 790_000;
/** Align with ci:check:design-system production CSS cap (92 KiB). */
const DEFAULT_CSS_BUDGET_BYTES = 92 * 1024;
const MIN_VITE_NODE_MAJOR = 20;

function nodeMajorVersion() {
  return Number.parseInt(process.versions.node.split('.')[0], 10);
}

function readBudget(envName, defaultValue) {
  const raw = process.env[envName];
  if (!raw) {
    return defaultValue;
  }

  const parsed = Number.parseInt(raw, 10);
  if (Number.isNaN(parsed) || parsed <= 0) {
    throw new Error(`Invalid ${envName} value "${raw}". Expected a positive integer (bytes).`);
  }
  return parsed;
}

function formatKiB(bytes) {
  return `${(bytes / 1024).toFixed(2)} KiB`;
}

function runBuild() {
  execSync('pnpm exec vite build --config src/web-spa/vite.config.ts', {
    cwd: repoRoot,
    stdio: 'inherit',
  });
}

function loadManifest() {
  const raw = readFileSync(manifestPath, 'utf8');
  return JSON.parse(raw);
}

function main() {
  if (nodeMajorVersion() < MIN_VITE_NODE_MAJOR) {
    if (!existsSync(outputFile)) {
      throw new Error(
        `Node ${process.versions.node} cannot run the Vite SPA build (requires Node ${MIN_VITE_NODE_MAJOR}+). ` +
          'Commit src/web/spa-assets.ts or build on a newer Node version.',
      );
    }
    console.log(
      `Skipping Vite SPA build on Node ${process.versions.node}; using committed src/web/spa-assets.ts.`,
    );
    return;
  }

  rmSync(outDir, { recursive: true, force: true });
  runBuild();

  const manifest = loadManifest();
  const entry = manifest['index.html'];
  if (!entry?.file) {
    throw new Error('Missing index.html entry in Vite manifest');
  }
  const importedChunks = Array.isArray(entry.imports) ? entry.imports : [];
  const dynamicImportedChunks = Array.isArray(entry.dynamicImports) ? entry.dynamicImports : [];
  const jsChunks = [entry.file, ...importedChunks, ...dynamicImportedChunks];
  if (jsChunks.length > 1) {
    throw new Error(
      `Unsupported SPA build output: index.html resolves to multiple JS chunks (${jsChunks.join(', ')}). ` +
        'This worker only serves a single inlined /app/assets/spa.js bundle.',
    );
  }
  if (Array.isArray(entry.css) && entry.css.length > 1) {
    throw new Error(
      `Unsupported SPA build output: index.html resolves to multiple CSS files (${entry.css.join(', ')}). ` +
        'This worker only serves a single inlined /app/assets/spa.css bundle.',
    );
  }

  const jsPath = resolve(outDir, entry.file);
  const cssFile = Array.isArray(entry.css) && entry.css.length > 0 ? entry.css[0] : null;
  const cssPath = cssFile ? resolve(outDir, cssFile) : null;

  const jsContent = readFileSync(jsPath, 'utf8');
  const cssContent = cssPath ? readFileSync(cssPath, 'utf8') : '';
  const jsBytes = Buffer.byteLength(jsContent, 'utf8');
  const cssBytes = Buffer.byteLength(cssContent, 'utf8');
  const jsBudget = readBudget('SPA_JS_BUDGET_BYTES', DEFAULT_JS_BUDGET_BYTES);
  const cssBudget = readBudget('SPA_CSS_BUDGET_BYTES', DEFAULT_CSS_BUDGET_BYTES);

  if (jsBytes > jsBudget || cssBytes > cssBudget) {
    const failures = [];
    if (jsBytes > jsBudget) {
      failures.push(
        `JS ${formatKiB(jsBytes)} > budget ${formatKiB(jsBudget)} (SPA_JS_BUDGET_BYTES)`,
      );
    }
    if (cssBytes > cssBudget) {
      failures.push(
        `CSS ${formatKiB(cssBytes)} > budget ${formatKiB(cssBudget)} (SPA_CSS_BUDGET_BYTES)`,
      );
    }

    throw new Error(
      `SPA payload budget exceeded:\n- ${failures.join('\n- ')}\n` +
        `Set SPA_JS_BUDGET_BYTES and/or SPA_CSS_BUDGET_BYTES to adjust thresholds.`,
    );
  }

  const buildId = createHash('sha256')
    .update(jsContent)
    .update('\n')
    .update(cssContent)
    .digest('hex')
    .slice(0, 16);

  const generated =
    `// This file is auto-generated by scripts/web/build-spa-assets.mjs\n` +
    `export const SPA_BUILD_ID = ${JSON.stringify(buildId)};\n` +
    `export const SPA_JS = ${JSON.stringify(jsContent)};\n` +
    `export const SPA_CSS = ${JSON.stringify(cssContent)};\n`;

  let current = '';
  try {
    current = readFileSync(outputFile, 'utf8');
  } catch {
    current = '';
  }

  if (current !== generated) {
    writeFileSync(outputFile, generated, 'utf8');
  }
  rmSync(outDir, { recursive: true, force: true });

  console.log(`Generated ${outputFile}`);
}

main();
