#!/usr/bin/env node
// Build-time SEO linter (ADR-0003, docs/seo-aeo-optimization.md §1b).
// Walks dist/**/*.html and fails (exit 1) on structural SEO violations that the Zod frontmatter
// schema cannot see in rendered HTML. Dependency-free.
import { readdirSync, readFileSync, statSync } from "node:fs";
import { join, relative } from "node:path";

const DIST = "dist";
const failures = [];

function walk(dir) {
  const out = [];
  for (const entry of readdirSync(dir)) {
    const full = join(dir, entry);
    if (statSync(full).isDirectory()) out.push(...walk(full));
    else if (entry.endsWith(".html")) out.push(full);
  }
  return out;
}

function fail(file, msg) {
  failures.push(`${relative(DIST, file)}: ${msg}`);
}

function checkFile(file) {
  const html = readFileSync(file, "utf8");

  // Exactly one <h1>
  const h1s = html.match(/<h1[\s>]/gi) ?? [];
  if (h1s.length !== 1) fail(file, `expected exactly 1 <h1>, found ${h1s.length}`);

  // <title> present + non-empty
  const title = html.match(/<title>([^<]*)<\/title>/i);
  if (!title || !title[1].trim()) fail(file, "missing or empty <title>");

  // meta description present + non-empty
  const desc = html.match(/<meta[^>]*name=["']description["'][^>]*>/i);
  if (!desc) fail(file, "missing <meta name=description>");
  else {
    const content = desc[0].match(/content=["']([^"']*)["']/i);
    if (!content || !content[1].trim()) fail(file, "empty meta description");
  }

  // canonical present
  if (!/<link[^>]*rel=["']canonical["'][^>]*>/i.test(html)) fail(file, "missing <link rel=canonical>");

  // every <img> has non-empty alt (decorative images must opt out via aria-hidden/role=presentation)
  for (const tag of html.match(/<img\b[^>]*>/gi) ?? []) {
    const decorative = /\baria-hidden=["']true["']/i.test(tag) || /\brole=["']presentation["']/i.test(tag);
    if (decorative) continue;
    const alt = tag.match(/\balt=["']([^"']*)["']/i);
    if (!alt) fail(file, `<img> without alt: ${tag.slice(0, 80)}`);
    else if (!alt[1].trim()) fail(file, `<img> with empty alt: ${tag.slice(0, 80)}`);
  }

  // every JSON-LD block parses
  for (const m of html.matchAll(/<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi)) {
    try {
      JSON.parse(m[1]);
    } catch (e) {
      fail(file, `invalid JSON-LD: ${e.message}`);
    }
  }

  // heading order: never skip a level going deeper
  const levels = [...html.matchAll(/<h([1-6])[\s>]/gi)].map((m) => Number(m[1]));
  for (let i = 1; i < levels.length; i++) {
    if (levels[i] > levels[i - 1] + 1) {
      fail(file, `heading jumps from h${levels[i - 1]} to h${levels[i]}`);
      break;
    }
  }
}

let files = [];
try {
  files = walk(DIST);
} catch {
  console.error(`seo-lint: no ${DIST}/ directory — run \`npm run build\` first.`);
  process.exit(2);
}

for (const f of files) checkFile(f);

if (failures.length) {
  console.error(`\nSEO lint FAILED — ${failures.length} issue(s) across ${files.length} page(s):\n`);
  for (const f of failures) console.error(`  ✗ ${f}`);
  process.exit(1);
}
console.log(`SEO lint passed — ${files.length} page(s) clean.`);
