#!/usr/bin/env node
// add-sri.mjs — 构建后为导出 HTML 中的本地 <script>/<link rel=stylesheet> 注入
// Subresource Integrity (sha384) + crossorigin。满足 doc/05 §2「用户中心开启 SRI」。
// 纯静态导出：Next 不内置 SRI,故在 out/ 上后处理。外链(Google Fonts 等)不注入,由 CSP 约束。
import { createHash } from 'node:crypto';
import { readFile, writeFile, readdir, stat } from 'node:fs/promises';
import { join, normalize } from 'node:path';

const OUT = 'out';

async function walk(dir) {
  const entries = await readdir(dir, { withFileTypes: true });
  const files = [];
  for (const e of entries) {
    const p = join(dir, e.name);
    if (e.isDirectory()) files.push(...(await walk(p)));
    else if (e.name.endsWith('.html')) files.push(p);
  }
  return files;
}

const cache = new Map();
async function sriFor(localPath) {
  if (cache.has(localPath)) return cache.get(localPath);
  try {
    const buf = await readFile(localPath);
    const hash = 'sha384-' + createHash('sha384').update(buf).digest('base64');
    cache.set(localPath, hash);
    return hash;
  } catch {
    cache.set(localPath, null);
    return null;
  }
}

function isLocal(url) {
  return url && !/^https?:\/\//i.test(url) && !url.startsWith('//') && !url.startsWith('data:');
}

async function resolveAsset(url) {
  const clean = url.split('?')[0].split('#')[0];
  const rel = clean.startsWith('/') ? clean.slice(1) : clean;
  const candidate = normalize(join(OUT, rel));
  try {
    await stat(candidate);
    return candidate;
  } catch {
    return null;
  }
}

async function processHtml(file) {
  let html = await readFile(file, 'utf8');
  let count = 0;

  // <script ... src="...">
  const scriptRe = /<script\b[^>]*\bsrc=["']([^"']+)["'][^>]*><\/script>/gi;
  const linkRe = /<link\b[^>]*\brel=["']stylesheet["'][^>]*>/gi;

  const replacements = [];
  let m;
  while ((m = scriptRe.exec(html))) replacements.push({ tag: m[0], url: m[1] });
  while ((m = linkRe.exec(html))) {
    const href = /href=["']([^"']+)["']/i.exec(m[0]);
    if (href) replacements.push({ tag: m[0], url: href[1] });
  }

  for (const { tag, url } of replacements) {
    if (!isLocal(url) || /integrity=/.test(tag)) continue;
    const asset = await resolveAsset(url);
    if (!asset) continue;
    const hash = await sriFor(asset);
    if (!hash) continue;
    const newTag = tag.replace(/(\s*\/?>)(\s*(?:<\/script>)?)\s*$/, (mm, close, rest) => ` integrity="${hash}" crossorigin="anonymous"${close}${rest}`);
    if (newTag !== tag) {
      html = html.replace(tag, newTag);
      count++;
    }
  }

  await writeFile(file, html);
  return count;
}

async function main() {
  let files;
  try {
    files = await walk(OUT);
  } catch {
    console.error(`[sri] 找不到 ${OUT}/，请先 next build`);
    process.exit(1);
  }
  let total = 0;
  for (const f of files) total += await processHtml(f);
  console.log(`[sri] 处理 ${files.length} 个 HTML，注入 integrity ${total} 处`);
}

main();
