#!/usr/bin/env node
/**
 * PostToolUse hook: verify config files after Edit/Write.
 * Runs the relevant single scanner on the edited file.
 * Blocks if new critical/high findings are introduced.
 * Timeout: 10 seconds (runs one scanner, not all 8).
 * Graceful degradation: returns {} (allow) on any error.
 */

import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { basename, dirname, resolve, sep } from 'node:path';
import { createHash } from 'node:crypto';
import { tmpdir } from 'node:os';

// Config file patterns (shared with auto-backup-config.mjs)
const CONFIG_PATTERNS = [
  { pattern: /CLAUDE\.md$/i, scanner: 'CML' },
  { pattern: /CLAUDE\.local\.md$/i, scanner: 'CML' },
  { pattern: /settings\.json$/, scanner: 'SET' },
  { pattern: /settings\.local\.json$/, scanner: 'SET' },
  { pattern: /hooks\.json$/, scanner: 'HKV' },
  { pattern: /\.mcp\.json$/, scanner: 'MCP' },
];

const RULES_DIR_PATTERN = /[/\\]rules[/\\]/;

function detectScanner(filePath) {
  if (!filePath) return null;
  const name = basename(filePath);
  const dir = dirname(filePath);

  for (const { pattern, scanner } of CONFIG_PATTERNS) {
    if (pattern.test(name)) return scanner;
  }

  // Rules directory
  if ((RULES_DIR_PATTERN.test(dir) || dir.endsWith(`${sep}rules`)) && name.endsWith('.md')) {
    return 'RUL';
  }

  return null;
}

function getCacheKey(filePath) {
  const hash = createHash('md5').update(filePath).digest('hex').slice(0, 8);
  return resolve(tmpdir(), `config-audit-last-scan-${hash}.json`);
}

function loadPreviousScan(cacheFile) {
  try {
    if (existsSync(cacheFile)) {
      return JSON.parse(readFileSync(cacheFile, 'utf-8'));
    }
  } catch { /* ignore */ }
  return null;
}

function saveScanResult(cacheFile, result) {
  try {
    writeFileSync(cacheFile, JSON.stringify(result), 'utf-8');
  } catch { /* ignore */ }
}

/**
 * Read all data from stdin asynchronously.
 * @returns {Promise<string>}
 */
function readStdin() {
  return new Promise((resolve, reject) => {
    const chunks = [];
    process.stdin.setEncoding('utf-8');
    process.stdin.on('data', chunk => chunks.push(chunk));
    process.stdin.on('end', () => resolve(chunks.join('')));
    process.stdin.on('error', reject);
    // Safety: if no data arrives within 2s, resolve with empty string
    setTimeout(() => resolve(chunks.join('')), 2000);
  });
}

function allow() {
  process.stdout.write('{}');
  process.exit(0);
}

/**
 * Walk up from filePath to find a likely project root.
 */
function findProjectRoot(fp) {
  let dir = dirname(resolve(fp));
  for (let i = 0; i < 10; i++) {
    if (existsSync(resolve(dir, '.git')) || existsSync(resolve(dir, 'CLAUDE.md'))) {
      return dir;
    }
    const parent = dirname(dir);
    if (parent === dir) break;
    dir = parent;
  }
  return dirname(resolve(fp));
}

async function main() {
  // Read stdin
  let raw;
  try {
    raw = await readStdin();
  } catch {
    allow();
    return;
  }

  // Parse tool input
  let toolInput;
  try {
    toolInput = JSON.parse(raw);
  } catch {
    allow();
    return;
  }

  const filePath = toolInput.file_path || toolInput.path;
  const scannerType = detectScanner(filePath);

  if (!scannerType || !filePath || !existsSync(filePath)) {
    allow();
    return;
  }

  // Run the relevant scanner
  const projectDir = findProjectRoot(filePath);
  const { discoverConfigFiles } = await import('../../scanners/lib/file-discovery.mjs');
  const { resetCounter } = await import('../../scanners/lib/output.mjs');

  const scannerMap = {
    CML: () => import('../../scanners/claude-md-linter.mjs'),
    SET: () => import('../../scanners/settings-validator.mjs'),
    HKV: () => import('../../scanners/hook-validator.mjs'),
    RUL: () => import('../../scanners/rules-validator.mjs'),
    MCP: () => import('../../scanners/mcp-config-validator.mjs'),
  };

  const loader = scannerMap[scannerType];
  if (!loader) {
    allow();
    return;
  }

  resetCounter();
  const { scan } = await loader();
  const discovery = await discoverConfigFiles(projectDir, { includeGlobal: false });
  const result = await scan(projectDir, discovery);

  // Compare with previous scan
  const cacheFile = getCacheKey(filePath);
  const previous = loadPreviousScan(cacheFile);

  // Save current result
  saveScanResult(cacheFile, {
    criticalCount: result.counts.critical || 0,
    highCount: result.counts.high || 0,
    findingCount: result.findings.length,
  });

  if (!previous) {
    allow();
    return;
  }

  // Check if new critical/high findings were introduced
  const newCritical = (result.counts.critical || 0) - (previous.criticalCount || 0);
  const newHigh = (result.counts.high || 0) - (previous.highCount || 0);

  if (newCritical > 0 || newHigh > 0) {
    const parts = [];
    if (newCritical > 0) parts.push(`${newCritical} new critical`);
    if (newHigh > 0) parts.push(`${newHigh} new high`);

    const response = {
      decision: 'block',
      reason: `[config-audit] Edit introduced ${parts.join(' and ')} finding(s) in ${basename(filePath)}. Review with /config-audit posture`,
    };
    process.stdout.write(JSON.stringify(response));
  } else {
    process.stdout.write('{}');
  }
}

main().catch(() => {
  // Graceful degradation — always allow on error
  process.stdout.write('{}');
  process.exit(0);
});
