#!/usr/bin/env bun
/**
 * Hoist Astro's island runtime <style> into <head> for every built HTML file.
 *
 * Astro injects `<style>astro-island,astro-slot,astro-static-slot{display:contents}</style>`
 * inline next to the first client:* directive on a page. That lands inside
 * whatever element wraps the island (often a <div>), which fails HTML5
 * conformance: the `scoped` attribute was removed from the spec, so <style>
 * is only valid inside <head> or <noscript>.
 *
 * Browsers and Googlebot ignore the violation, but W3C validator.nu and SEO
 * audit tools (sitechecker, etc.) flag it as a critical markup error on
 * every page. This post-build step rewrites the output to move the tag
 * into <head>, keeping audit reports clean without touching Astro core.
 *
 * Port of astro6-seo-template/scripts/hoist-astro-styles.mjs.
 */
import { parseArgs } from "node:util";
import { resolve } from "node:path";

// Matches the exact style Astro emits — anchored to the known payload to avoid
// clobbering author styles that happen to target astro-island.
const STYLE_RE = /<style>astro-island,astro-slot,astro-static-slot\{display:contents\}<\/style>/g;

const STYLE_TAG = "<style>astro-island,astro-slot,astro-static-slot{display:contents}</style>";

export interface RunOptions {
  directory: string;
  dryRun: boolean;
  quiet: boolean;
}

export interface RunResult {
  rewritten: number;
  total: number;
  skippedNoHead: string[];
}

function log(quiet: boolean, msg: string): void {
  if (!quiet) console.log(msg);
}

export async function run(opts: RunOptions): Promise<RunResult> {
  const root = resolve(opts.directory);
  const glob = new Bun.Glob("**/*.html");

  const files: string[] = [];
  for await (const entry of glob.scan({ cwd: root, absolute: true })) {
    files.push(entry);
  }

  let rewritten = 0;
  const skippedNoHead: string[] = [];

  for (const file of files) {
    const html = await Bun.file(file).text();
    STYLE_RE.lastIndex = 0;
    if (!STYLE_RE.test(html)) continue;
    STYLE_RE.lastIndex = 0;

    if (!/<\/head>/.test(html)) {
      log(
        opts.quiet,
        `[hoist-astro-styles] warning: ${file} contains the offending <style> but has no </head>; skipping`,
      );
      skippedNoHead.push(file);
      continue;
    }

    const stripped = html.replace(STYLE_RE, "");
    const injected = stripped.replace(/<\/head>/, `${STYLE_TAG}</head>`);

    if (injected !== html) {
      if (!opts.dryRun) {
        await Bun.write(file, injected);
      }
      rewritten++;
    }
  }

  const prefix = opts.dryRun
    ? "[hoist-astro-styles] (dry-run) would rewrite"
    : "[hoist-astro-styles] rewrote";
  console.log(`${prefix} ${rewritten} / ${files.length} HTML files`);

  return { rewritten, total: files.length, skippedNoHead };
}

function printUsageAndExit(code: number): never {
  console.error(
    "Usage: hoist-astro-styles <directory> [--dry-run] [--quiet]\n\n" +
      "Hoist Astro's inline island <style> tag into <head> for every .html file\n" +
      "found recursively under <directory>.\n",
  );
  process.exit(code);
}

async function main(): Promise<void> {
  let parsed;
  try {
    parsed = parseArgs({
      args: process.argv.slice(2),
      options: {
        "dry-run": { type: "boolean", default: false },
        quiet: { type: "boolean", default: false },
        help: { type: "boolean", default: false, short: "h" },
      },
      allowPositionals: true,
      strict: true,
    });
  } catch (err) {
    console.error(`[hoist-astro-styles] ${(err as Error).message}`);
    printUsageAndExit(1);
  }

  if (parsed.values.help) {
    printUsageAndExit(0);
  }

  const directory = parsed.positionals[0];
  if (!directory) {
    printUsageAndExit(1);
  }

  await run({
    directory,
    dryRun: parsed.values["dry-run"] === true,
    quiet: parsed.values.quiet === true,
  });
}

if (import.meta.main) {
  await main();
}
