/**
 * check-outdated.mjs — pnpm outdated advisory-only tiered report (D-06 / OQ-01).
 *
 * Classifies all outdated packages into four tiers in priority order:
 *  1. AUDIT-ADVISORY    — the package's current version carries a known advisory
 *  2. MAJOR-BEHIND-INTENTIONAL — latest major > current major, pin reason exists in outdated-pins.json
 *  3. MAJOR-BEHIND-UNPINNED    — latest major > current major, no pin reason (potential liability)
 *  4. ROUTINE-DRIFT             — same major, minor/patch behind (low priority)
 *
 * This script ALWAYS exits 0 — it is advisory-only and never gates the build (D-06).
 */

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

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

/**
 * Run a command and return stdout, tolerating non-zero exit codes.
 * pnpm outdated exits non-zero when any package is outdated — we need the output anyway.
 *
 * @param {string} cmd
 * @returns {string}
 */
function captureOutput(cmd) {
  try {
    return execSync(cmd, {
      encoding: 'utf8',
      stdio: ['pipe', 'pipe', 'ignore'],
    });
  } catch (err) {
    // Non-zero exit — pnpm outdated does this when packages are outdated
    return err.stdout ?? '';
  }
}

/**
 * Parse the major version integer from a semver string.
 * Returns 0 if parsing fails.
 *
 * @param {string} version
 * @returns {number}
 */
function majorOf(version) {
  const n = parseInt((version ?? '').split('.')[0], 10);
  return isNaN(n) ? 0 : n;
}

// ── Load pin reasons ─────────────────────────────────────────────────────────
const pinsPath = resolve(__dirname, 'outdated-pins.json');
let pins = {};
try {
  pins = JSON.parse(readFileSync(pinsPath, 'utf8'));
} catch {
  // Gracefully degrade — no pins means everything is treated as unpinned
  console.warn('[check-outdated] Warning: could not read outdated-pins.json; treating all pins as unknown');
}

// ── Run pnpm audit to collect vulnerable module names ───────────────────────
let vulnerableModules = new Set();
try {
  const auditOutput = captureOutput('pnpm audit --json');
  const auditData = JSON.parse(auditOutput);
  const advisories = auditData.advisories ?? {};
  for (const adv of Object.values(advisories)) {
    if (adv.module_name) {
      vulnerableModules.add(adv.module_name);
    }
  }
} catch {
  // Audit parse failure is non-fatal for the outdated report
  console.warn('[check-outdated] Warning: could not parse pnpm audit output; AUDIT-ADVISORY cross-check skipped');
}

// ── Run pnpm outdated ────────────────────────────────────────────────────────
const outdatedOutput = captureOutput('pnpm outdated --format json -r');

let outdatedData = {};
if (outdatedOutput.trim()) {
  try {
    outdatedData = JSON.parse(outdatedOutput);
  } catch {
    console.warn('[check-outdated] Warning: could not parse pnpm outdated JSON output');
  }
}

// ── Classify entries into tiers ──────────────────────────────────────────────
const tiers = {
  auditAdvisory: [],
  majorBehindIntentional: [],
  majorBehindUnpinned: [],
  routineDrift: [],
};

for (const [pkgName, info] of Object.entries(outdatedData)) {
  const current = info.current ?? '';
  const latest = info.latest ?? '';
  const currentMajor = majorOf(current);
  const latestMajor = majorOf(latest);
  const isMajorBehind = latestMajor > currentMajor;
  const pinReason = pins[pkgName];
  const hasAdvisory = vulnerableModules.has(pkgName);

  const entry = {
    name: pkgName,
    current,
    latest,
    dependencyType: info.dependencyType ?? '',
    dependentPackages: info.dependentPackages,
  };

  // Priority 1: the package has an active advisory on the pinned version
  if (hasAdvisory) {
    tiers.auditAdvisory.push(entry);
  // Priority 2: major behind + intentional pin
  } else if (isMajorBehind && pinReason) {
    tiers.majorBehindIntentional.push({ ...entry, reason: pinReason });
  // Priority 3: major behind without a pin reason — possible liability
  } else if (isMajorBehind) {
    tiers.majorBehindUnpinned.push(entry);
  // Priority 4: same major, minor/patch drift
  } else {
    tiers.routineDrift.push(entry);
  }
}

// ── Print human-readable report ──────────────────────────────────────────────
console.log('');
console.log('=== DEPENDENCY HEALTH REPORT ===');
console.log('');

// Tier 1: AUDIT-ADVISORY
console.log('[AUDIT-ADVISORY] Packages with active advisories on the pinned version:');
if (tiers.auditAdvisory.length === 0) {
  console.log('  (none)');
} else {
  for (const pkg of tiers.auditAdvisory) {
    console.log(`  ${pkg.name}  ${pkg.current} → ${pkg.latest}  (${pkg.dependencyType}) *** ADVISORY ON CURRENT VERSION ***`);
  }
}
console.log('');

// Tier 2: MAJOR-BEHIND / INTENTIONAL PIN
console.log('[MAJOR-BEHIND / INTENTIONAL PIN] Packages behind due to a known constraint:');
if (tiers.majorBehindIntentional.length === 0) {
  console.log('  (none)');
} else {
  for (const pkg of tiers.majorBehindIntentional) {
    console.log(`  ${pkg.name}  ${pkg.current} → ${pkg.latest}  (${pkg.dependencyType})`);
    console.log(`    reason: ${pkg.reason}`);
  }
}
console.log('');

// Tier 3: MAJOR-BEHIND / UNPINNED
console.log('[MAJOR-BEHIND / UNPINNED] Packages >1 major behind without a pin reason:');
if (tiers.majorBehindUnpinned.length === 0) {
  console.log('  (none)');
} else {
  for (const pkg of tiers.majorBehindUnpinned) {
    console.log(`  ${pkg.name}  ${pkg.current} → ${pkg.latest}  (${pkg.dependencyType})`);
  }
}
console.log('');

// Tier 4: ROUTINE-DRIFT
console.log('[ROUTINE-DRIFT] Patch/minor updates (low priority):');
if (tiers.routineDrift.length === 0) {
  console.log('  (none)');
} else {
  const items = tiers.routineDrift.map((p) => `${p.name} ${p.current} → ${p.latest}`);
  console.log('  ' + items.join(', '));
}
console.log('');

// Advisory-only — NEVER gates the build (D-06)
process.exit(0);
