#!/usr/bin/env node
// Regenerates packages/core/paths/reserved-slugs.ts from
// server/internal/handler/reserved_slugs.json (the single source of truth).
//
// Run via `pnpm generate:reserved-slugs`. CI re-runs the generator and
// `git diff --exit-code`s the output, so the JSON and TS sides cannot drift.

import { readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const jsonPath = resolve(repoRoot, "server/internal/handler/reserved_slugs.json");
const tsPath = resolve(repoRoot, "packages/core/paths/reserved-slugs.ts");

const raw = readFileSync(jsonPath, "utf8");
const data = JSON.parse(raw);

if (!Array.isArray(data.groups)) {
  throw new Error(`reserved_slugs.json: expected top-level "groups" array`);
}

const lines = [];
lines.push("// AUTO-GENERATED by scripts/generate-reserved-slugs.mjs.");
lines.push("// Do not edit by hand — edit server/internal/handler/reserved_slugs.json");
lines.push("// and run `pnpm generate:reserved-slugs`.");
lines.push("");
lines.push("/**");
lines.push(" * Slugs reserved because they collide with frontend top-level routes,");
lines.push(" * platform features, or web standards.");
lines.push(" *");
lines.push(" * Single source of truth: `server/internal/handler/reserved_slugs.json`.");
lines.push(" * The Go backend embeds that JSON; this file is regenerated from it.");
lines.push(" *");
lines.push(" * Convention for new global routes (CLAUDE.md): use a single word");
lines.push(" * (`/login`, `/inbox`) or `/{noun}/{verb}` (`/workspaces/new`). Hyphenated");
lines.push(" * root-level word groups (`/new-workspace`, `/create-team`) collide with");
lines.push(" * common user workspace names — see PR for full discussion.");
lines.push(" */");
lines.push("export const RESERVED_SLUGS: ReadonlySet<string> = new Set([");

const seen = new Set();
for (const [i, group] of data.groups.entries()) {
  if (!group.label || !Array.isArray(group.slugs)) {
    throw new Error(
      `reserved_slugs.json: each group must have a "label" string and "slugs" array`,
    );
  }
  if (i > 0) lines.push("");
  lines.push(`  // ${group.label}`);
  if (typeof group.description === "string" && group.description.length > 0) {
    for (const wrapped of wrapComment(group.description, 76)) {
      lines.push(`  // ${wrapped}`);
    }
  }
  for (const slug of group.slugs) {
    if (typeof slug !== "string" || slug.length === 0) {
      throw new Error(`reserved_slugs.json: slug entries must be non-empty strings`);
    }
    if (seen.has(slug)) {
      throw new Error(`reserved_slugs.json: duplicate slug ${JSON.stringify(slug)}`);
    }
    seen.add(slug);
    lines.push(`  ${JSON.stringify(slug)},`);
  }
}

lines.push("]);");
lines.push("");
lines.push("export function isReservedSlug(slug: string): boolean {");
lines.push("  return RESERVED_SLUGS.has(slug);");
lines.push("}");
lines.push("");

writeFileSync(tsPath, lines.join("\n"));

// Wrap a single-line description into comment lines no wider than `width`,
// breaking on whitespace only. Tokens longer than `width` are kept intact.
function wrapComment(text, width) {
  const tokens = text.split(/\s+/).filter(Boolean);
  const out = [];
  let current = "";
  for (const tok of tokens) {
    if (current.length === 0) {
      current = tok;
    } else if (current.length + 1 + tok.length <= width) {
      current += ` ${tok}`;
    } else {
      out.push(current);
      current = tok;
    }
  }
  if (current.length > 0) out.push(current);
  return out;
}
