/**
 * Permission-matrix audit.
 *
 * Walks every src/app/api/v1/** /route.ts file and reports each exported HTTP
 * handler (GET/POST/PUT/PATCH/DELETE) that is *not* wrapped in withPermission().
 * Internal v1 routes should be permission-gated; routes that intentionally use
 * withAuth() alone (e.g. user-self endpoints) can be allow-listed below.
 *
 * Run:
 *   pnpm tsx scripts/audit-permissions.ts
 *
 * Exit code:
 *   0 — every handler is permission-gated or in the allow-list
 *   1 — at least one handler is missing both a withPermission wrapper and an
 *       allow-list entry. CI should fail.
 */

import { readdir, readFile } from 'node:fs/promises';
import { join, relative } from 'node:path';

const ROOT = join(process.cwd(), 'src/app/api/v1');
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const;

/**
 * Routes intentionally exempt from withPermission. Each entry should explain
 * why — typically because the route operates on the caller's own resources
 * (no port-level permission semantics) or is admin-only and gated by
 * isSuperAdmin inside the handler.
 */
const ALLOW_LIST: ReadonlyArray<{ pattern: RegExp; reason: string }> = [
  // Self / admin / public
  { pattern: /\/me\/route\.ts$/, reason: 'Self-endpoint — auth is sufficient.' },
  { pattern: /\/admin\//, reason: 'Admin-only — gated by isSuperAdmin inside handler.' },
  {
    pattern: /\/notifications\//,
    reason: 'User-scoped notifications — caller is the resource owner.',
  },
  { pattern: /\/socket\//, reason: 'Socket auth handshake.' },
  { pattern: /\/health\//, reason: 'Public health check.' },
  { pattern: /\/users\/me\//, reason: 'User-self preferences — caller is the resource owner.' },
  { pattern: /\/saved-views\//, reason: 'User-self saved views — caller is the resource owner.' },
  {
    pattern: /\/settings\/feature-flag\//,
    reason: 'Public read of feature-flag bool — no PII; auth is sufficient.',
  },
  // Cross-cutting / port-scoped reference data
  { pattern: /\/tags\//, reason: 'Tags are cross-cutting reference data; port-scoped via auth.' },
  {
    pattern: /\/currency\/(convert|rates)\/route\.ts$/,
    reason: 'Currency reference data; port-scoped, no PII.',
  },
  {
    pattern: /\/currency\/rates\/refresh\//,
    reason: 'TODO: gate with admin:manage_settings — currently allow-listed.',
  },
  {
    pattern: /\/search\//,
    reason: 'Port-scoped search — results filtered by auth context (resources have own perms).',
  },
  // Alerts surface in topbar/dashboard for every signed-in user; per-port not per-resource.
  { pattern: /\/alerts\//, reason: 'Alerts are user-scoped; port-filtered via auth context.' },
  // Internally gated by isSuperAdmin
  {
    pattern: /\/expenses\/export\/parent-company\//,
    reason: 'Internally gated by isSuperAdmin inside the handler.',
  },
  // Pending dedicated permissions
  {
    pattern: /\/ai\//,
    reason: 'TODO: needs ai:* permission catalog entry. Currently allow-listed.',
  },
  {
    pattern: /\/custom-fields\/\[entityId\]\//,
    reason: 'TODO: needs custom_fields:* permission. PUT path internally validated.',
  },
];

interface Finding {
  file: string;
  method: string;
  reason: 'no-withPermission' | 'no-withAuth' | 'allow-listed';
  allowReason?: string;
}

async function* walk(dir: string): AsyncGenerator<string> {
  for (const entry of await readdir(dir, { withFileTypes: true })) {
    const path = join(dir, entry.name);
    if (entry.isDirectory()) yield* walk(path);
    else if (entry.isFile() && entry.name === 'route.ts') yield path;
  }
}

function isAllowListed(file: string): { allowed: boolean; reason?: string } {
  for (const { pattern, reason } of ALLOW_LIST) {
    if (pattern.test(file)) return { allowed: true, reason };
  }
  return { allowed: false };
}

async function auditFile(file: string): Promise<Finding[]> {
  const src = await readFile(file, 'utf-8');
  const findings: Finding[] = [];

  for (const method of HTTP_METHODS) {
    // Match: export const GET = withAuth(...
    const declRe = new RegExp(`export\\s+const\\s+${method}\\s*=\\s*(.+?);`, 's');
    const m = declRe.exec(src);
    if (!m) continue;
    const block = m[1] ?? '';

    const hasAuth = /withAuth\s*\(/.test(block);
    const hasPerm = /withPermission\s*\(/.test(block);
    const allow = isAllowListed(file);

    if (!hasAuth) {
      findings.push({ file, method, reason: 'no-withAuth' });
      continue;
    }
    if (!hasPerm) {
      if (allow.allowed) {
        findings.push({ file, method, reason: 'allow-listed', allowReason: allow.reason });
      } else {
        findings.push({ file, method, reason: 'no-withPermission' });
      }
    }
  }

  return findings;
}

async function main() {
  const files: string[] = [];
  for await (const f of walk(ROOT)) files.push(f);
  files.sort();

  const all: Finding[] = [];
  for (const f of files) all.push(...(await auditFile(f)));

  const violations = all.filter(
    (f) => f.reason === 'no-withPermission' || f.reason === 'no-withAuth',
  );
  const allowListed = all.filter((f) => f.reason === 'allow-listed');

  // Markdown report
  const lines: string[] = [];
  lines.push('# Permission Matrix Audit');
  lines.push('');
  lines.push(`Scanned ${files.length} route files under \`src/app/api/v1/\`.`);
  lines.push('');

  if (violations.length === 0) {
    lines.push('**No violations.** Every internal v1 handler is permission-gated.');
  } else {
    lines.push(`**${violations.length} violation(s):**`);
    lines.push('');
    lines.push('| File | Method | Issue |');
    lines.push('| --- | --- | --- |');
    for (const v of violations) {
      const rel = relative(process.cwd(), v.file);
      lines.push(`| \`${rel}\` | ${v.method} | ${v.reason} |`);
    }
  }
  lines.push('');
  lines.push(
    `**Allow-listed:** ${allowListed.length} handler(s) intentionally skip \`withPermission\`.`,
  );
  if (allowListed.length > 0) {
    lines.push('');
    lines.push('| File | Method | Reason |');
    lines.push('| --- | --- | --- |');
    for (const a of allowListed) {
      const rel = relative(process.cwd(), a.file);
      lines.push(`| \`${rel}\` | ${a.method} | ${a.allowReason} |`);
    }
  }

  process.stdout.write(lines.join('\n') + '\n');
  process.exit(violations.length > 0 ? 1 : 0);
}

main().catch((err) => {
  console.error(err);
  process.exit(2);
});
