#!/usr/bin/env bun
import plugin from "bun-plugin-tailwind";
import { existsSync } from "fs";
import { rm } from "fs/promises";
import path from "path";

if (process.argv.includes("--help") || process.argv.includes("-h")) {
  console.log(`
🏗️  Bun Build Script

Usage: bun run build.ts [options]

Common Options:
  --outdir <path>          Output directory (default: "dist")
  --minify                 Enable minification (or --minify.whitespace, --minify.syntax, etc)
  --sourcemap <type>      Sourcemap type: none|linked|inline|external
  --target <target>        Build target: browser|bun|node
  --format <format>        Output format: esm|cjs|iife
  --splitting              Enable code splitting
  --packages <type>        Package handling: bundle|external
  --public-path <path>     Public path for assets
  --env <mode>             Environment handling: inline|disable|prefix*
  --conditions <list>      Package.json export conditions (comma separated)
  --external <list>        External packages (comma separated)
  --banner <text>          Add banner text to output
  --footer <text>          Add footer text to output
  --define <obj>           Define global constants (e.g. --define.VERSION=1.0.0)
  --help, -h               Show this help message

Example:
  bun run build.ts --outdir=dist --minify --sourcemap=linked --external=react,react-dom
`);
  process.exit(0);
}

const toCamelCase = (str: string): string => str.replace(/-([a-z])/g, g => g[1]?.toUpperCase() || "");

const parseValue = (value: string): any => {
  if (value === "true") return true;
  if (value === "false") return false;

  if (/^\d+$/.test(value)) return parseInt(value, 10);
  if (/^\d*\.\d+$/.test(value)) return parseFloat(value);

  if (value.includes(",")) return value.split(",").map(v => v.trim());

  return value;
};

function parseArgs(): Partial<Bun.BuildConfig> {
  const config: Partial<Bun.BuildConfig> = {};
  const args = process.argv.slice(2);

  for (let i = 0; i < args.length; i++) {
    const arg = args[i];
    if (arg === undefined) continue;
    if (!arg.startsWith("--")) continue;

    if (arg.startsWith("--no-")) {
      const key = toCamelCase(arg.slice(5));
      // @ts-ignore
      config[key] = false;
      continue;
    }

    if (!arg.includes("=") && (i === args.length - 1 || args[i + 1]?.startsWith("--"))) {
      const key = toCamelCase(arg.slice(2));
      // @ts-ignore
      config[key] = true;
      continue;
    }

    let key: string;
    let value: string;

    if (arg.includes("=")) {
      [key, value] = arg.slice(2).split("=", 2) as [string, string];
    } else {
      key = arg.slice(2);
      value = args[++i] ?? "";
    }

    key = toCamelCase(key);

    if (key.includes(".")) {
      const [parentKey, childKey] = key.split(".");
      // @ts-ignore
      config[parentKey] = config[parentKey] || {};
      // @ts-ignore
      config[parentKey][childKey] = parseValue(value);
    } else {
      // @ts-ignore
      config[key] = parseValue(value);
    }
  }

  return config;
}

const formatFileSize = (bytes: number): string => {
  const units = ["B", "KB", "MB", "GB"];
  let size = bytes;
  let unitIndex = 0;

  while (size >= 1024 && unitIndex < units.length - 1) {
    size /= 1024;
    unitIndex++;
  }

  return `${size.toFixed(2)} ${units[unitIndex]}`;
};

console.log("\n🚀 Starting build process...\n");

const cliConfig = parseArgs();
const outdir = cliConfig.outdir || path.join(process.cwd(), "dist");

if (existsSync(outdir)) {
  console.log(`🗑️ Cleaning previous build at ${outdir}`);
  await rm(outdir, { recursive: true, force: true });
}

const start = performance.now();

const entrypoints = [...new Bun.Glob("**.html").scanSync("src")]
  .map(a => path.resolve("src", a))
  .filter(dir => !dir.includes("node_modules"));
console.log(`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`);

const build = async () => {
  const result = await Bun.build({
    entrypoints,
    outdir,
    plugins: [plugin],
    minify: true,
    target: "browser",
    sourcemap: "linked",
    publicPath: "/", // Use absolute paths for SPA routing compatibility
    define: {
      "process.env.NODE_ENV": JSON.stringify((cliConfig as any).watch ? "development" : "production"),
    },
    ...cliConfig,
  });

  const outputTable = result.outputs.map(output => ({
    File: path.relative(process.cwd(), output.path),
    Type: output.kind,
    Size: formatFileSize(output.size),
  }));

  console.table(outputTable);
  return result;
};

const result = await build();

const end = performance.now();
const buildTime = (end - start).toFixed(2);
console.log(`\n✅ Build completed in ${buildTime}ms\n`);

if ((cliConfig as any).watch) {
  console.log("👀 Watching for changes...\n");

  // Polling-based file watcher for Docker compatibility
  // Docker volumes don't propagate filesystem events (inotify) reliably
  const srcDir = path.join(process.cwd(), "src");
  const POLL_INTERVAL_MS = 1000;
  let lastMtimes = new Map<string, number>();
  let isRebuilding = false;

  // Collect all file mtimes in src directory
  const collectMtimes = async (): Promise<Map<string, number>> => {
    const mtimes = new Map<string, number>();
    const glob = new Bun.Glob("**/*.{ts,tsx,js,jsx,css,html}");
    
    for await (const file of glob.scan({ cwd: srcDir, absolute: true })) {
      try {
        const stat = await Bun.file(file).stat();
        if (stat) {
          mtimes.set(file, stat.mtime.getTime());
        }
      } catch {
        // File may have been deleted, skip
      }
    }
    return mtimes;
  };

  // Initial collection
  lastMtimes = await collectMtimes();

  // Polling loop
  const poll = async () => {
    if (isRebuilding) return;

    const currentMtimes = await collectMtimes();
    const changedFiles: string[] = [];

    // Check for new or modified files
    for (const [file, mtime] of currentMtimes) {
      const lastMtime = lastMtimes.get(file);
      if (lastMtime === undefined || lastMtime < mtime) {
        changedFiles.push(path.relative(srcDir, file));
      }
    }

    // Check for deleted files
    for (const file of lastMtimes.keys()) {
      if (!currentMtimes.has(file)) {
        changedFiles.push(path.relative(srcDir, file) + " (deleted)");
      }
    }

    if (changedFiles.length > 0) {
      isRebuilding = true;
      console.log(`\n🔄 Changes detected:`);
      changedFiles.forEach(f => console.log(`   • ${f}`));
      console.log("");

      try {
        const rebuildStart = performance.now();
        await build();
        const rebuildEnd = performance.now();
        console.log(`\n✅ Rebuild completed in ${(rebuildEnd - rebuildStart).toFixed(2)}ms\n`);
      } catch (err) {
        console.error("❌ Rebuild failed:", err);
      }

      lastMtimes = currentMtimes;
      isRebuilding = false;
    }
  };

  const interval = setInterval(poll, POLL_INTERVAL_MS);

  // Handle manual exit
  process.on("SIGINT", () => {
    clearInterval(interval);
    console.log("\n👋 Stopping build watcher...");
    process.exit(0);
  });

  // Keep process alive
  process.stdin.resume();
}
