/**
 * check-audit.mjs — pnpm audit wrapper for CI dependency gate (D-04 / D-05).
 *
 * Exports pure filter functions (selectBlocking, partitionAdvisories) so the
 * logic can be unit-tested without spawning pnpm. The main body (run only when
 * invoked directly via import.meta.url) reads the committed allowlist, runs
 * `pnpm audit --json`, and exits 1 if any unwaived High/Critical advisories exist.
 *
 * Rules:
 *  - Uses `pnpm audit --json` with NO --audit-level (captures all severities).
 *    --audit-level would filter the JSON output itself (Pitfall 1).
 *  - Waives advisories listed in scripts/audit-allowlist.json by github_advisory_id.
 *  - Exits 0 when all High/Critical advisories are waived (or there are none).
 *  - Prints moderate/low advisory list to stdout as advisory-only info before exiting 0.
 */

import { execSync } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { resolve, dirname } from 'node:path';

const BLOCKING_SEVERITIES = new Set(['high', 'critical']);

/**
 * From an advisories map (keyed by numeric id), returns the subset that
 * are High or Critical AND whose github_advisory_id is NOT present in allowlist.
 *
 * @param {Record<string, {severity: string, github_advisory_id: string, module_name: string, title: string}>} advisories
 * @param {Record<string, {reason: string, reviewer: string, expires: string}>} allowlist
 * @returns {Array<{severity: string, github_advisory_id: string, module_name: string, title: string}>}
 */
export function selectBlocking(advisories, allowlist) {
  return Object.values(advisories).filter(
    (adv) =>
      BLOCKING_SEVERITIES.has(adv.severity) &&
      !allowlist[adv.github_advisory_id],
  );
}

/**
 * Partitions all advisories into blocking (unwaived High/Critical) and
 * advisory-only (moderate/low, or waived High/Critical).
 *
 * @param {Record<string, {severity: string, github_advisory_id: string, module_name: string, title: string}>} advisories
 * @param {Record<string, {reason: string, reviewer: string, expires: string}>} allowlist
 * @returns {{ blocking: Array, advisory: Array }}
 */
export function partitionAdvisories(advisories, allowlist) {
  const blocking = [];
  const advisory = [];

  for (const adv of Object.values(advisories)) {
    if (BLOCKING_SEVERITIES.has(adv.severity) && !allowlist[adv.github_advisory_id]) {
      blocking.push(adv);
    } else {
      advisory.push(adv);
    }
  }

  return { blocking, advisory };
}

// Main body — only runs when invoked directly (not when imported as a module).
const __filename = fileURLToPath(import.meta.url);
const isMain = process.argv[1] === __filename;

if (isMain) {
  const __dirname = dirname(__filename);
  const allowlistPath = resolve(__dirname, 'audit-allowlist.json');

  // Load the committed waiver allowlist.
  let allowlist;
  try {
    allowlist = JSON.parse(readFileSync(allowlistPath, 'utf8'));
  } catch (err) {
    console.error(`[check-audit] Failed to read allowlist at ${allowlistPath}: ${err.message}`);
    process.exit(2);
  }

  // Run pnpm audit --json without --audit-level so all severities appear in output.
  // stderr is suppressed (pnpm writes progress/warnings there); we only need stdout JSON.
  let auditOutput;
  try {
    auditOutput = execSync('pnpm audit --json', {
      encoding: 'utf8',
      stdio: ['pipe', 'pipe', 'ignore'],
    });
  } catch (err) {
    // pnpm audit exits non-zero when advisories exist — capture stdout from the error.
    auditOutput = err.stdout ?? '';
  }

  let auditData;
  try {
    auditData = JSON.parse(auditOutput);
  } catch (err) {
    console.error(`[check-audit] Failed to parse pnpm audit JSON output: ${err.message}`);
    process.exit(2);
  }

  const advisories = auditData.advisories ?? {};
  const { blocking, advisory } = partitionAdvisories(advisories, allowlist);

  if (blocking.length > 0) {
    console.error('BLOCKING advisories (High/Critical, not in allowlist):');
    for (const adv of blocking) {
      console.error(
        `  ${adv.github_advisory_id ?? '(no GHSA)'} [${adv.severity}] ${adv.module_name}: ${adv.title}`,
      );
    }
    process.exit(1);
  }

  console.log('Audit PASS — no unwaived High/Critical advisories.');

  if (advisory.length > 0) {
    console.log('Advisory (non-blocking) findings:');
    for (const adv of advisory) {
      console.log(
        `  ${adv.github_advisory_id ?? '(no GHSA)'} [${adv.severity}] ${adv.module_name}`,
      );
    }
  }

  process.exit(0);
}
