#!/usr/bin/env node

/**
 * Config-Audit Scan Orchestrator
 * Runs all registered scanners sequentially, collects findings, outputs JSON envelope.
 * Usage: node scan-orchestrator.mjs <target-path> [--output-file path] [--save-baseline] [--baseline path]
 * Zero external dependencies.
 */

import { resolve, sep } from 'node:path';
import { readFile, writeFile } from 'node:fs/promises';
import { resetCounter } from './lib/output.mjs';
import { envelope } from './lib/output.mjs';
import { discoverConfigFiles, discoverConfigFilesMulti, discoverFullMachinePaths } from './lib/file-discovery.mjs';
import { loadSuppressions, applySuppressions, formatSuppressionSummary } from './lib/suppression.mjs';
import { humanizeEnvelope } from './lib/humanizer.mjs';

// Scanner registry — import order determines execution order
import { scan as scanClaudeMd } from './claude-md-linter.mjs';
import { scan as scanSettings } from './settings-validator.mjs';
import { scan as scanHooks } from './hook-validator.mjs';
import { scan as scanRules } from './rules-validator.mjs';
import { scan as scanMcp } from './mcp-config-validator.mjs';
import { scan as scanImports } from './import-resolver.mjs';
import { scan as scanConflicts } from './conflict-detector.mjs';
import { scan as scanGap } from './feature-gap-scanner.mjs';
import { scan as scanTokenHotspots } from './token-hotspots.mjs';
import { scan as scanCachePrefix } from './cache-prefix-scanner.mjs';
import { scan as scanDisabledInSchema } from './disabled-in-schema-scanner.mjs';
import { scan as scanCollision } from './collision-scanner.mjs';

// Directory names that identify test fixture / example directories
const FIXTURE_DIR_NAMES = ['tests', 'examples', '__tests__', 'test-fixtures'];

/**
 * Check if a finding originates from a test fixture or example directory
 * relative to the scan target. Only filters when the finding's path extends
 * beyond the target into a fixture subdirectory — if the target itself is
 * a fixture directory, findings are NOT filtered.
 * @param {object} f - Finding object
 * @param {string} targetPath - Resolved scan target path
 * @returns {boolean}
 */
function isFixturePath(f, targetPath) {
  const p = f.file || f.path || f.location || '';
  if (!p || !p.startsWith(targetPath)) return false;
  // Get the path relative to target, then check if it passes through a fixture dir
  const rel = p.slice(targetPath.length);
  return FIXTURE_DIR_NAMES.some(dir => rel.includes(sep + dir + sep));
}

const SCANNERS = [
  { name: 'CML', fn: scanClaudeMd, label: 'CLAUDE.md Linter' },
  { name: 'SET', fn: scanSettings, label: 'Settings Validator' },
  { name: 'HKV', fn: scanHooks, label: 'Hook Validator' },
  { name: 'RUL', fn: scanRules, label: 'Rules Validator' },
  { name: 'MCP', fn: scanMcp, label: 'MCP Config Validator' },
  { name: 'IMP', fn: scanImports, label: 'Import Resolver' },
  { name: 'CNF', fn: scanConflicts, label: 'Conflict Detector' },
  { name: 'GAP', fn: scanGap, label: 'Feature Gap Scanner' },
  { name: 'TOK', fn: scanTokenHotspots, label: 'Token Hotspots' },
  { name: 'CPS', fn: scanCachePrefix, label: 'Cache-Prefix Stability' },
  { name: 'DIS', fn: scanDisabledInSchema, label: 'Disabled-In-Schema' },
  { name: 'COL', fn: scanCollision, label: 'Plugin Skill Collision' },
];

/**
 * Run all scanners against target path.
 * @param {string} targetPath
 * @param {object} [opts]
 * @param {boolean} [opts.includeGlobal=false]
 * @param {boolean} [opts.fullMachine=false] - Scan all known locations across the machine
 * @param {boolean} [opts.suppress=true] - Apply suppressions from .config-audit-ignore
 * @param {boolean} [opts.filterFixtures=true] - Exclude findings from test/example paths
 * @returns {Promise<object>} Full envelope with all results
 */
// Exported for testing
export { isFixturePath, FIXTURE_DIR_NAMES };

export async function runAllScanners(targetPath, opts = {}) {
  const start = Date.now();
  const resolvedPath = resolve(targetPath);

  // Shared file discovery — scanners reuse this
  let discovery;
  if (opts.fullMachine) {
    const roots = await discoverFullMachinePaths();
    discovery = await discoverConfigFilesMulti(roots);
  } else {
    discovery = await discoverConfigFiles(resolvedPath, {
      includeGlobal: opts.includeGlobal || false,
    });
  }

  const results = [];

  for (const scanner of SCANNERS) {
    resetCounter();
    const scanStart = Date.now();
    try {
      const result = await scanner.fn(resolvedPath, discovery);
      results.push(result);
      const count = result.findings.length;
      const label = opts.humanizedProgress
        ? `\`[${scanner.name}] ${scanner.label}\``
        : `[${scanner.name}] ${scanner.label}`;
      process.stderr.write(`  ${label}: ${count} finding(s) (${Date.now() - scanStart}ms)\n`);
    } catch (err) {
      results.push({
        scanner: scanner.name,
        status: 'error',
        files_scanned: 0,
        duration_ms: Date.now() - scanStart,
        findings: [],
        counts: { critical: 0, high: 0, medium: 0, low: 0, info: 0 },
        error: err.message,
      });
      const label = opts.humanizedProgress
        ? `\`[${scanner.name}] ${scanner.label}\``
        : `[${scanner.name}] ${scanner.label}`;
      process.stderr.write(`  ${label}: ERROR — ${err.message}\n`);
    }
  }

  // Filter findings from test fixtures / examples (unless disabled)
  const shouldFilterFixtures = opts.filterFixtures !== false;
  let fixtureFindings = [];

  if (shouldFilterFixtures) {
    for (const result of results) {
      const active = [];
      const fixture = [];
      for (const f of result.findings) {
        if (isFixturePath(f, resolvedPath)) {
          fixture.push(f);
        } else {
          active.push(f);
        }
      }
      if (fixture.length > 0) {
        fixtureFindings.push(...fixture);
        result.findings = active;
        result.counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
        for (const f of active) {
          if (result.counts[f.severity] !== undefined) result.counts[f.severity]++;
        }
      }
    }
    if (fixtureFindings.length > 0) {
      process.stderr.write(`  ${fixtureFindings.length} finding(s) from test fixtures excluded\n`);
    }
  }

  // Apply suppressions (unless disabled)
  const shouldSuppress = opts.suppress !== false;
  let suppressedFindings = [];

  if (shouldSuppress) {
    const { suppressions } = await loadSuppressions(resolvedPath);
    if (suppressions.length > 0) {
      for (const result of results) {
        const { active, suppressed } = applySuppressions(result.findings, suppressions);
        suppressedFindings.push(...suppressed);
        result.findings = active;
        // Recalculate counts
        result.counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
        for (const f of active) {
          if (result.counts[f.severity] !== undefined) result.counts[f.severity]++;
        }
      }
      if (suppressedFindings.length > 0) {
        process.stderr.write(`  ${formatSuppressionSummary(suppressedFindings)}\n`);
      }
    }
  }

  const totalMs = Date.now() - start;
  const env = envelope(resolvedPath, results, totalMs);
  if (fixtureFindings.length > 0) {
    env.fixture_findings = fixtureFindings;
  }
  if (suppressedFindings.length > 0) {
    env.suppressed_findings = suppressedFindings;
  }
  return env;
}

// --- CLI entry point ---
async function main() {
  const args = process.argv.slice(2);
  let targetPath = '.';
  let outputFile = null;
  let saveBaseline = false;
  let baselinePath = null;

  for (let i = 0; i < args.length; i++) {
    if (args[i] === '--output-file' && args[i + 1]) {
      outputFile = args[++i];
    } else if (args[i] === '--save-baseline') {
      saveBaseline = true;
    } else if (args[i] === '--baseline' && args[i + 1]) {
      baselinePath = args[++i];
    } else if (args[i] === '--global') {
      // handled below
    } else if (args[i] === '--full-machine') {
      // handled below
    } else if (args[i] === '--no-suppress') {
      // handled below
    } else if (args[i] === '--include-fixtures') {
      // handled below
    } else if (args[i] === '--json') {
      // handled below — explicit machine-readable mode (bypass humanizer)
    } else if (args[i] === '--raw') {
      // handled below — v5.0.0 verbatim mode (bypass humanizer)
    } else if (!args[i].startsWith('-')) {
      targetPath = args[i];
    }
  }

  const includeGlobal = args.includes('--global');
  const fullMachine = args.includes('--full-machine');
  const suppress = !args.includes('--no-suppress');
  const filterFixtures = !args.includes('--include-fixtures');
  const jsonMode = args.includes('--json');
  const rawMode = args.includes('--raw');

  const humanizedProgress = !jsonMode && !rawMode;
  process.stderr.write(humanizedProgress ? `Config-Audit v2.2.0\n` : `Config-Audit Scanner v2.2.0\n`);
  process.stderr.write(`Target: ${resolve(targetPath)}\n`);
  process.stderr.write(`Scope: ${fullMachine ? 'full-machine' : includeGlobal ? 'global' : 'project'}\n`);
  process.stderr.write(`Fixtures: ${filterFixtures ? 'excluded' : 'included'}\n\n`);

  const result = await runAllScanners(targetPath, {
    includeGlobal,
    fullMachine,
    suppress,
    filterFixtures,
    humanizedProgress,
  });

  // Default mode runs the humanizer; --json and --raw bypass for v5.0.0 byte-equal output.
  const output = (jsonMode || rawMode) ? result : humanizeEnvelope(result);
  const json = JSON.stringify(output, null, 2);

  if (outputFile) {
    await writeFile(outputFile, json, 'utf-8');
    process.stderr.write(`\nResults written to ${outputFile}\n`);
  } else {
    process.stdout.write(json + '\n');
  }

  if (saveBaseline) {
    const bPath = baselinePath || resolve(targetPath, '.config-audit-baseline.json');
    // Always save baselines as raw v5.0.0-shape envelope so future humanizer
    // changes don't trigger false-positive drift findings.
    await writeFile(bPath, JSON.stringify(result, null, 2), 'utf-8');
    process.stderr.write(`Baseline saved to ${bPath}\n`);
  }

  // Summary
  const agg = result.aggregate;
  process.stderr.write(`\n--- Summary ---\n`);
  process.stderr.write(`Findings: ${agg.total_findings} (C:${agg.counts.critical} H:${agg.counts.high} M:${agg.counts.medium} L:${agg.counts.low} I:${agg.counts.info})\n`);
  process.stderr.write(`Risk: ${agg.risk_score}/100 (${agg.risk_band})\n`);
  process.stderr.write(`Verdict: ${agg.verdict}\n`);

  // Exit code
  if (agg.verdict === 'FAIL') process.exit(2);
  if (agg.verdict === 'WARNING') process.exit(1);
  process.exit(0);
}

// Only run CLI if invoked directly
const isDirectRun = process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname);
if (isDirectRun) {
  main().catch(err => {
    process.stderr.write(`Fatal: ${err.message}\n`);
    process.exit(3);
  });
}
