#!/usr/bin/env node
/**
 * Enumerate every published error code (ADR 239 dotted `NAMESPACE.SUBCODE`)
 * defined in production source, for the docs error-reference page at
 * https://docs.prisma.io/docs/orm/next/reference/error-reference.
 *
 * Modes:
 *   node scripts/list-error-codes.mjs                       # JSON to stdout
 *   node scripts/list-error-codes.mjs --format markdown     # reference skeleton
 *   node scripts/list-error-codes.mjs --verify <page.md>    # exit 1 + list any
 *                                                           # known code missing
 *                                                           # from the given file
 *   --root <dir>   repo root to scan (default: this script's parent repo);
 *                  lets the docs-site repo run it against a checkout of this repository.
 *
 * Codes are string literals whose namespace is on the ADR 239 closed list.
 * Scanned: git-tracked .ts files under each package's src tree (production
 * source only — tests assert codes, they don't define them).
 */

import { execFileSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { argv, exit, stderr, stdout } from 'node:process';
import { fileURLToPath } from 'node:url';

const NAMESPACES = [
  'CONFIG',
  'CLI',
  'CONTRACT',
  'PSL',
  'PLAN',
  'RUNTIME',
  'ORM',
  'DRIVER',
  'BUDGET',
  'LINT',
  'MIGRATION',
  'SUPABASE',
  'POSTGIS',
  'PGVECTOR',
  'PARADEDB',
  'TESTKIT',
];

const CODE_RE = new RegExp(`'((?:${NAMESPACES.join('|')})\\.[A-Z][A-Z0-9_]*)'`, 'g');

export function extractCodes(root) {
  const files = execFileSync('git', ['ls-files', 'packages/**/*.ts'], {
    cwd: root,
    encoding: 'utf-8',
  })
    .split('\n')
    .filter((f) => f.includes('/src/') && !/\.(test|test-d)\.ts$/.test(f) && !f.includes('/test/'));

  const codes = new Map();
  for (const file of files) {
    const text = readFileSync(join(root, file), 'utf-8');
    for (const match of text.matchAll(CODE_RE)) {
      const code = match[1];
      if (!codes.has(code)) codes.set(code, new Set());
      codes.get(code).add(file);
    }
  }
  return [...codes.entries()]
    .map(([code, fileSet]) => ({
      code,
      namespace: code.split('.')[0],
      files: [...fileSet].sort(),
    }))
    .sort((a, b) => a.code.localeCompare(b.code));
}

export function toMarkdown(entries) {
  const lines = ['# Error reference', ''];
  let current = '';
  for (const entry of entries) {
    if (entry.namespace !== current) {
      current = entry.namespace;
      lines.push(`## ${current}`, '');
    }
    lines.push(`### ${entry.code}`, '');
  }
  return lines.join('\n');
}

export function verify(entries, pageText) {
  return entries.filter((e) => !pageText.includes(e.code)).map((e) => e.code);
}

function main() {
  const args = argv.slice(2);
  const readFlag = (name) => {
    const i = args.indexOf(name);
    return i === -1 ? undefined : args[i + 1];
  };

  const root = readFlag('--root') ?? join(fileURLToPath(new URL('.', import.meta.url)), '..');
  const entries = extractCodes(root);

  const verifyPath = readFlag('--verify');
  if (verifyPath !== undefined) {
    const missing = verify(entries, readFileSync(verifyPath, 'utf-8'));
    if (missing.length > 0) {
      stderr.write(
        `error-reference is missing ${missing.length} of ${entries.length} known codes:\n`,
      );
      for (const code of missing) stderr.write(`  ${code}\n`);
      exit(1);
    }
    stdout.write(`error-reference lists all ${entries.length} known codes.\n`);
    return;
  }

  const format = readFlag('--format') ?? 'json';
  stdout.write(
    format === 'markdown' ? `${toMarkdown(entries)}\n` : `${JSON.stringify(entries, null, 2)}\n`,
  );
}

if (process.argv[1] === fileURLToPath(import.meta.url)) main();