#!/usr/bin/env node
// reference-config-generator.mjs — Generate Grade A security configuration
// Runs posture-scanner, identifies gaps, generates settings/CLAUDE.md/.gitignore
// to close them. Supports --dry-run (default) and --apply (writes files with backup).
//
// Standalone CLI: node scanners/reference-config-generator.mjs [path] [--apply] [--dry-run]
// Library: import { generate } from './reference-config-generator.mjs'
//
// Zero external dependencies — Node.js builtins only.

import { readFile, writeFile, mkdir, access, copyFile } from 'node:fs/promises';
import { join, resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { scan } from './posture-scanner.mjs';
import { resetCounter } from './lib/output.mjs';

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

const __dirname = dirname(fileURLToPath(import.meta.url));
const TEMPLATES_DIR = resolve(__dirname, '..', 'templates', 'reference-config');

// Categories where we can generate config fixes
const FIXABLE_CATEGORIES = new Map([
  [1, 'settings'],    // Deny-First Configuration
  [2, 'gitignore'],   // Secrets Protection (gitignore portion)
  [3, 'gitignore'],   // Path Guarding (gitignore portion)
  [6, 'settings'],    // Sandbox Configuration
  [10, 'claudemd'],   // Cognitive State Security (CLAUDE.md guardrails)
]);

// ---------------------------------------------------------------------------
// File helpers
// ---------------------------------------------------------------------------

async function readJson(filePath) {
  try {
    return JSON.parse(await readFile(filePath, 'utf-8'));
  } catch { return null; }
}

async function readText(filePath) {
  try { return await readFile(filePath, 'utf-8'); }
  catch { return null; }
}

async function fileExists(filePath) {
  try { await access(filePath); return true; }
  catch { return false; }
}

async function ensureDir(dirPath) {
  await mkdir(dirPath, { recursive: true });
}

// ---------------------------------------------------------------------------
// Project type detection
// ---------------------------------------------------------------------------

/**
 * Detect project type: plugin, monorepo, or standalone.
 * @param {string} projectRoot
 * @returns {Promise<'plugin' | 'monorepo' | 'standalone'>}
 */
async function detectProjectType(projectRoot) {
  // Plugin: has .claude-plugin/plugin.json or plugin.json
  if (await fileExists(join(projectRoot, '.claude-plugin', 'plugin.json'))) return 'plugin';
  if (await fileExists(join(projectRoot, 'plugin.json'))) return 'plugin';

  // Monorepo: package.json with workspaces, or lerna.json, or pnpm-workspace.yaml
  const pkg = await readJson(join(projectRoot, 'package.json'));
  if (pkg?.workspaces) return 'monorepo';
  if (await fileExists(join(projectRoot, 'lerna.json'))) return 'monorepo';
  if (await fileExists(join(projectRoot, 'pnpm-workspace.yaml'))) return 'monorepo';

  return 'standalone';
}

// ---------------------------------------------------------------------------
// Recommendation builders
// ---------------------------------------------------------------------------

/**
 * Build settings.json recommendation.
 * If existing settings have deny-first, returns 'none'. Otherwise creates or merges.
 */
async function buildSettingsRec(projectRoot, categories) {
  const settingsPath = join(projectRoot, '.claude', 'settings.json');
  const existing = await readJson(settingsPath);
  const template = await readJson(join(TEMPLATES_DIR, 'settings-deny-first.json'));

  // Check if deny-first already set
  const cat1 = categories.find(c => c.id === 1);
  const cat6 = categories.find(c => c.id === 6);
  const needsDenyFirst = cat1 && cat1.status !== 'PASS';
  const needsSandboxFix = cat6 && cat6.status !== 'PASS' && cat6.status !== 'N_A';

  if (!needsDenyFirst && !needsSandboxFix) {
    return { category: 'Deny-First + Sandbox', file: '.claude/settings.json', action: 'none', content: '' };
  }

  if (!existing) {
    // Create fresh from template
    return {
      category: 'Deny-First + Sandbox',
      file: '.claude/settings.json',
      action: 'create',
      content: JSON.stringify(template, null, 2),
    };
  }

  // Merge: preserve existing keys, add deny-first if missing
  const merged = { ...existing };
  if (needsDenyFirst) {
    if (!merged.permissions) merged.permissions = {};
    if (merged.permissions.defaultPermissionLevel !== 'deny' && merged.permissions.defaultPermissionLevel !== 'deny-all') {
      merged.permissions.defaultPermissionLevel = 'deny';
    }
    if (!merged.permissions.allow || merged.permissions.allow.includes('*')) {
      merged.permissions.allow = template.permissions.allow;
    }
  }
  if (needsSandboxFix) {
    if (merged.skipDangerousModePermissionPrompt === true) {
      merged.skipDangerousModePermissionPrompt = false;
    }
    if (merged.dangerouslyAllowArbitraryPaths === true) {
      delete merged.dangerouslyAllowArbitraryPaths;
    }
  }

  return {
    category: 'Deny-First + Sandbox',
    file: '.claude/settings.json',
    action: 'merge',
    content: JSON.stringify(merged, null, 2),
  };
}

/**
 * Build CLAUDE.md recommendation.
 * Appends security section if not already present.
 */
async function buildClaudeMdRec(projectRoot, categories) {
  const claudeMdPath = join(projectRoot, 'CLAUDE.md');
  const existing = await readText(claudeMdPath);
  const template = await readText(join(TEMPLATES_DIR, 'claude-md-security-section.md'));

  // Check categories that benefit from CLAUDE.md guardrails
  const cat1 = categories.find(c => c.id === 1);
  const cat7 = categories.find(c => c.id === 7);
  const cat10 = categories.find(c => c.id === 10);

  // If already has security boundaries, skip
  if (existing && /security\s+boundar/i.test(existing)) {
    return { category: 'CLAUDE.md Security', file: 'CLAUDE.md', action: 'none', content: '' };
  }

  const needsSection = (cat1 && cat1.status !== 'PASS') ||
                        (cat7 && cat7.status !== 'PASS') ||
                        (cat10 && cat10.status !== 'PASS') ||
                        !existing;

  if (!needsSection) {
    return { category: 'CLAUDE.md Security', file: 'CLAUDE.md', action: 'none', content: '' };
  }

  if (!existing) {
    return {
      category: 'CLAUDE.md Security',
      file: 'CLAUDE.md',
      action: 'create',
      content: `# Project\n\n${template}`,
    };
  }

  return {
    category: 'CLAUDE.md Security',
    file: 'CLAUDE.md',
    action: 'append',
    content: `\n${template}`,
  };
}

/**
 * Build .gitignore recommendation.
 * Adds missing security patterns.
 */
async function buildGitignoreRec(projectRoot, categories) {
  const gitignorePath = join(projectRoot, '.gitignore');
  const existing = await readText(gitignorePath);
  const template = await readText(join(TEMPLATES_DIR, 'gitignore-security.txt'));
  const templateLines = template.trim().split('\n').filter(l => l.trim() && !l.startsWith('#'));

  const cat2 = categories.find(c => c.id === 2);
  const cat9 = categories.find(c => c.id === 9);

  if (!existing) {
    const needsGitignore = (cat2 && cat2.status !== 'PASS') ||
                            (cat9 && cat9.status !== 'PASS');
    if (!needsGitignore) {
      return { category: 'Secrets + Session', file: '.gitignore', action: 'none', content: '' };
    }
    return {
      category: 'Secrets + Session',
      file: '.gitignore',
      action: 'create',
      content: template,
    };
  }

  // Find missing lines
  const missingLines = templateLines.filter(line => !existing.includes(line.trim()));

  if (missingLines.length === 0) {
    return { category: 'Secrets + Session', file: '.gitignore', action: 'none', content: '' };
  }

  return {
    category: 'Secrets + Session',
    file: '.gitignore',
    action: 'append',
    content: '\n# Security additions (llm-security harden)\n' + missingLines.join('\n') + '\n',
  };
}

// ---------------------------------------------------------------------------
// Apply logic
// ---------------------------------------------------------------------------

/**
 * Apply recommendations to the filesystem.
 * @param {string} projectRoot
 * @param {object[]} recommendations
 * @returns {Promise<string|null>} backupPath or null
 */
async function applyRecommendations(projectRoot, recommendations) {
  const actionable = recommendations.filter(r => r.action !== 'none');
  if (actionable.length === 0) return null;

  // Create backup of files we'll modify
  const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
  const backupDir = join(projectRoot, `.security-harden-backup-${ts}`);
  let backedUp = false;

  for (const rec of actionable) {
    const filePath = join(projectRoot, rec.file);
    if (await fileExists(filePath)) {
      if (!backedUp) {
        await ensureDir(backupDir);
        backedUp = true;
      }
      const backupFile = join(backupDir, rec.file.replace(/\//g, '__'));
      await copyFile(filePath, backupFile);
    }
  }

  // Apply each recommendation
  for (const rec of actionable) {
    const filePath = join(projectRoot, rec.file);

    switch (rec.action) {
      case 'create': {
        await ensureDir(dirname(filePath));
        await writeFile(filePath, rec.content, 'utf-8');
        break;
      }
      case 'merge': {
        // For settings.json: write the merged content
        await ensureDir(dirname(filePath));
        await writeFile(filePath, rec.content, 'utf-8');
        break;
      }
      case 'append': {
        const existing = await readText(filePath) || '';
        await writeFile(filePath, existing + rec.content, 'utf-8');
        break;
      }
    }
  }

  return backedUp ? backupDir : null;
}

// ---------------------------------------------------------------------------
// Main generate function
// ---------------------------------------------------------------------------

/**
 * Generate reference configuration for a project.
 * @param {string} targetPath - Absolute path to project root
 * @param {object} [options]
 * @param {boolean} [options.apply=false] - Write files to disk
 * @param {boolean} [options.dryRun=true] - Show what would change (default)
 * @returns {Promise<object>} Generation result
 */
export async function generate(targetPath, options = {}) {
  const apply = options.apply === true;
  const startMs = Date.now();
  const projectRoot = resolve(targetPath);

  // Step 1: Detect project type
  const projectType = await detectProjectType(projectRoot);

  // Step 2: Run posture scan
  resetCounter();
  const postureResult = await scan(projectRoot);
  const categories = postureResult.categories;

  // Step 3: Build recommendations
  const recommendations = [];
  recommendations.push(await buildSettingsRec(projectRoot, categories));
  recommendations.push(await buildClaudeMdRec(projectRoot, categories));
  recommendations.push(await buildGitignoreRec(projectRoot, categories));

  // Step 4: Apply if requested
  let backupPath = null;
  let applied = false;
  if (apply) {
    backupPath = await applyRecommendations(projectRoot, recommendations);
    applied = true;
  }

  const durationMs = Date.now() - startMs;

  return {
    status: 'ok',
    target: projectRoot,
    projectType,
    timestamp: new Date().toISOString(),
    duration_ms: durationMs,
    posture: {
      grade: postureResult.scoring.grade,
      pass_rate: postureResult.scoring.pass_rate,
      pass: postureResult.scoring.pass,
      partial: postureResult.scoring.partial,
      fail: postureResult.scoring.fail,
    },
    recommendations,
    applied,
    backupPath,
    summary: {
      total: recommendations.length,
      actionable: recommendations.filter(r => r.action !== 'none').length,
      creates: recommendations.filter(r => r.action === 'create').length,
      merges: recommendations.filter(r => r.action === 'merge').length,
      appends: recommendations.filter(r => r.action === 'append').length,
    },
  };
}

// ---------------------------------------------------------------------------
// CLI entry point
// ---------------------------------------------------------------------------

const isMain = process.argv[1] && resolve(process.argv[1]) === resolve(fileURLToPath(import.meta.url));

if (isMain) {
  const args = process.argv.slice(2);
  const applyFlag = args.includes('--apply');
  const pathArg = args.find(a => !a.startsWith('--')) || process.cwd();
  const absTarget = resolve(pathArg);

  try {
    const result = await generate(absTarget, { apply: applyFlag });
    process.stdout.write(JSON.stringify(result, null, 2) + '\n');
    process.exit(0);
  } catch (err) {
    process.stderr.write(`Error: ${err.message}\n`);
    process.exit(2);
  }
}
