#!/usr/bin/env node
// build.js -- SunScope production bundler
//
// Usage:  node build.js   OR   npm run build
//
// What it does:
//   1. Bundles + minifies JS (entry: assets/js/main.js) -> dist/assets/bundle.min.js
//   2. Resolves CSS @imports + minifies -> dist/assets/bundle.min.css
//   3. Copies index.html / about.html / faq.html to dist/, rewriting script+link tags
//   4. Copies static files (images, robots.txt, sitemap.xml)
//
// Dev files are NEVER touched. Deploy from dist/ for production.

import * as esbuild from 'esbuild';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const ROOT   = __dirname;
const DIST   = path.join(ROOT, 'dist');
const ASSETS = path.join(DIST, 'assets');

// ---- Helpers ---------------------------------------------------------------

function copyFile(src, dest) {
  fs.mkdirSync(path.dirname(dest), { recursive: true });
  fs.copyFileSync(src, dest);
}

// Delete specific output files without trying to rmdir the folders
// (rmdir on Windows-mounted paths can fail inside Linux sandboxes)
function cleanDist() {
  const targets = [
    path.join(ASSETS, 'bundle.min.js'),
    path.join(ASSETS, 'bundle.min.css'),
    path.join(DIST,   'index.html'),
    path.join(DIST,   'about.html'),
    path.join(DIST,   'faq.html'),
    path.join(ASSETS, '_tmp_flat.css'),
    path.join(ASSETS, 'about.min.css'),
  ];
  for (const f of targets) {
    try { fs.unlinkSync(f); } catch (_) { /* ok if missing */ }
  }
}

// Recursively resolve CSS @import statements into one flat string
function resolveCSSImports(filePath, visited = new Set()) {
  const resolved = path.resolve(filePath);
  if (visited.has(resolved)) return '';
  visited.add(resolved);
  const dir     = path.dirname(resolved);
  const content = fs.readFileSync(resolved, 'utf8');
  return content.replace(/@import\s+["']([^"']+)["'];?/g, (_, importPath) => {
    const importFile = path.join(dir, importPath);
    if (!fs.existsSync(importFile)) {
      console.warn('  WARNING: CSS @import not found: ' + importFile);
      return '';
    }
    return resolveCSSImports(importFile, visited);
  });
}

// Short cache-buster string
function bumpVersion() {
  return Math.floor(Date.now() / 1000).toString(36);
}

// Swap dev asset tags in HTML for bundle tags.
// Strips the main sunscope.css link, rewrites standalone about.css to
// about.min.css with a version param, and injects the JS bundle.
function rewriteHTML(html, version) {
  html = html.replace(/<link\s[^>]*href=["'][^"']*assets\/sunscope\.css[^"']*["'][^>]*>/gi, '');
  html = html.replace(/<script\s[^>]*src=["'][^"']*assets\/js\/[^"']*["'][^>]*><\/script>/gi, '');
  // Rewrite standalone about.css reference to versioned minified file.
  html = html.replace(
    /(<link\s[^>]*href=["'][^"']*assets\/)about\.css(["'][^>]*>)/gi,
    '$1about.min.css?v=' + version + '$2'
  );
  const css = '  <link rel="stylesheet" href="./assets/bundle.min.css?v=' + version + '">';
  const js  = '  <script defer src="./assets/bundle.min.js?v=' + version + '"></script>';
  html = html.replace('</head>', css + '\n' + js + '\n</head>');
  return html;
}

// esbuild plugin: strips JSX block comments {/* ... */} that may contain
// backticks or other characters that trip up esbuild's JS parser.
// Safe to remove -- these are only developer notes, never runtime code.
const stripJSXCommentsPlugin = {
  name: 'strip-jsx-comments',
  setup(build) {
    build.onLoad({ filter: /\.js$/ }, async (args) => {
      let source = await fs.promises.readFile(args.path, 'utf8');
      // Remove {/* ... */} comment blocks (non-greedy, dotAll)
      source = source.replace(/\{\/\*[\s\S]*?\*\/\}/g, '');
      return { contents: source, loader: 'js' };
    });
  },
};

// ---- Main ------------------------------------------------------------------

async function build() {
  const t0 = Date.now();
  console.log('SunScope build starting...');

  cleanDist();
  fs.mkdirSync(ASSETS, { recursive: true });
  console.log('  Output dir ready: dist/');

  // 2. Bundle + minify JavaScript
  console.log('  Bundling JS...');
  await esbuild.build({
    entryPoints: [path.join(ROOT, 'assets/js/main.js')],
    bundle:      true,
    minify:      true,
    format:      'iife',
    globalName:  'SunScope',
    outfile:     path.join(ASSETS, 'bundle.min.js'),
    logLevel:    'warning',
    plugins:     [stripJSXCommentsPlugin],
  });
  const jsSize = fs.statSync(path.join(ASSETS, 'bundle.min.js')).size;
  console.log('  JS bundle: ' + (jsSize / 1024).toFixed(1) + ' KB');

  // 3. Resolve CSS @imports then minify
  console.log('  Bundling CSS...');
  const flatCSS = resolveCSSImports(path.join(ROOT, 'assets/sunscope.css'));
  const tmpCSS  = path.join(os.tmpdir(), '_sunscope_flat.css');
  fs.writeFileSync(tmpCSS, flatCSS, 'utf8');
  await esbuild.build({
    entryPoints: [tmpCSS],
    bundle:      false,
    minify:      true,
    outfile:     path.join(ASSETS, 'bundle.min.css'),
    logLevel:    'warning',
  });
  fs.unlinkSync(tmpCSS);
  const cssSize = fs.statSync(path.join(ASSETS, 'bundle.min.css')).size;
  console.log('  CSS bundle: ' + (cssSize / 1024).toFixed(1) + ' KB');

  // 4. Process HTML files
  console.log('  Processing HTML...');
  const version = bumpVersion();
  for (const htmlFile of ['index.html', 'about.html', 'faq.html']) {
    const src = path.join(ROOT, htmlFile);
    if (!fs.existsSync(src)) continue;
    const updated = rewriteHTML(fs.readFileSync(src, 'utf8'), version);
    fs.writeFileSync(path.join(DIST, htmlFile), updated, 'utf8');
    console.log('    -> ' + htmlFile);
  }

  // 5. Minify standalone CSS files that are not part of the main bundle
  // src name -> output name
  const standaloneCSS = [['about.css', 'about.min.css']];
  for (const [srcName, outName] of standaloneCSS) {
    const src = path.join(ROOT, 'assets', srcName);
    if (fs.existsSync(src)) {
      fs.mkdirSync(ASSETS, { recursive: true });
      await esbuild.build({
        entryPoints: [src],
        bundle:      false,
        minify:      true,
        outfile:     path.join(ASSETS, outName),
        logLevel:    'warning',
      });
      const sz = fs.statSync(path.join(ASSETS, outName)).size;
      console.log('    -> assets/' + outName + ' (' + (sz / 1024).toFixed(1) + ' KB minified)');
    }
  }

  // 6. Copy fonts - explicit allowlist keeps only what is referenced in CSS.
  // All variants remain in assets/fonts/ as source files.
  const FONT_ALLOWLIST = new Set([
    'manrope-variable.woff2',
    'fraunces-300-italic.woff2',
    'fraunces-700-normal.woff2',
    'fraunces-700-italic.woff2',
    'fraunces-900-normal.woff2',
    'jetbrains-mono-400.woff2',
    'jetbrains-mono-600.woff2',
  ]);
  console.log('  Copying fonts...');
  const fontsDir = path.join(ROOT, 'assets', 'fonts');
  if (fs.existsSync(fontsDir)) {
    let copied = 0;
    for (const f of fs.readdirSync(fontsDir)) {
      if (!FONT_ALLOWLIST.has(f)) continue;
      copyFile(path.join(fontsDir, f), path.join(DIST, 'assets', 'fonts', f));
      copied++;
    }
    console.log('    -> assets/fonts/ (' + copied + ' of ' + fs.readdirSync(fontsDir).length + ' files)');
  }

  // 7. Copy images
  console.log('  Copying images...');
  const imagesDir = path.join(ROOT, 'assets', 'images');
  if (fs.existsSync(imagesDir)) {
    const copyDirRecursive = (src, dest) => {
      fs.mkdirSync(dest, { recursive: true });
      for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
        const srcPath  = path.join(src,  entry.name);
        const destPath = path.join(dest, entry.name);
        if (entry.isDirectory()) copyDirRecursive(srcPath, destPath);
        else fs.copyFileSync(srcPath, destPath);
      }
    };
    copyDirRecursive(imagesDir, path.join(DIST, 'assets', 'images'));
    const imageCount = fs.readdirSync(path.join(imagesDir, 'profiles')).length;
    console.log('    -> assets/images/ (' + imageCount + ' profile images)');
  }

  // 8. Copy static files
  console.log('  Copying static files...');
  for (const f of ['robots.txt', 'sitemap.xml', 'og-image.png', 'favicon.svg']) {
    const src = path.join(ROOT, f);
    if (fs.existsSync(src)) {
      copyFile(src, path.join(DIST, f));
      console.log('    -> ' + f);
    }
  }

  const elapsed = ((Date.now() - t0) / 1000).toFixed(2);
  console.log('');
  console.log('========================================');
  console.log('  Build complete in ' + elapsed + 's');
  console.log('  Output: dist/');
  console.log('  JS:  ' + (jsSize  / 1024).toFixed(1) + ' KB');
  console.log('  CSS: ' + (cssSize / 1024).toFixed(1) + ' KB');
  console.log('========================================');
  console.log('  Deploy the dist/ folder to production.');
  console.log('  Dev files in assets/ are untouched.');
  console.log('========================================');
}

build().catch(err => {
  console.error('Build failed: ' + err.message);
  process.exit(1);
});
