// Copyright (c) 2026 defconxt. All rights reserved.
// Licensed under AGPL-3.0 — see LICENSE file for details.
// NERF is a trademark of defconxt.

/**
 * commands.js — Node.js handler functions for all 28 CLI commands.
 *
 * Each handler accepts a plain args object and returns a plain JS object.
 * No Rich formatting, no Typer framework — just data. The rendering
 * layer stays in nerf.js's formatBridgeResult().
 *
 * All module imports are lazy (dynamic import()) to preserve cold start.
 *
 * @module gateway/commands
 */

import { readFileSync, existsSync, readdirSync, writeFileSync, statSync } from 'node:fs';
import { join, dirname, resolve } from 'node:path';
import { execSync, execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { homedir } from 'node:os';

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

const debug = process.env.NERF_DEBUG === '1'
  ? (/** @type {string} */ msg) => process.stderr.write(`[bridge:node] commands: ${msg}\n`)
  : () => {};

/**
 * Default memory directory — mirrors Python's ~/.nerf/memory/default.
 * @returns {string}
 */
function defaultMemoryDir() {
  return join(homedir(), '.nerf', 'memory', 'default');
}

/**
 * Extract a value from CLI args array or object.
 * Handles both `nerf search lateral movement` (array) and `{query: "..."}` (object).
 * @param {string[]|object} args
 * @param {string} key - object key (e.g. 'query')
 * @param {string} [flagName] - CLI flag (e.g. '--query'). If null, uses positional args.
 * @returns {string}
 */
function getArg(args, key, flagName) {
  if (!Array.isArray(args)) return args[key] || '';
  if (flagName) {
    const idx = args.indexOf(flagName);
    if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
  }
  // Positional: join all non-flag args
  return args.filter(a => !a.startsWith('-')).join(' ');
}

/**
 * Extract a flag value from CLI args.
 * @param {string[]|object} args
 * @param {string} key - object key
 * @param {string} flag - CLI flag (e.g. '--limit')
 * @param {*} defaultVal
 * @returns {*}
 */
function getFlagArg(args, key, flag, defaultVal) {
  if (!Array.isArray(args)) return args[key] ?? defaultVal;
  const idx = args.indexOf(flag);
  if (idx !== -1 && idx + 1 < args.length) return args[idx + 1];
  return defaultVal;
}

/**
 * Resolve the repository root by walking up from this file.
 * @returns {string}
 */
function findRepoRoot() {
  let dir = resolve(__dirname, '..', '..');
  for (let i = 0; i < 10; i++) {
    if ((existsSync(join(dir, 'skills')) || existsSync(join(dir, 'src', 'resources', 'skills'))) && (existsSync(join(dir, 'cli')) || existsSync(join(dir, 'package.json')))) {
      return dir;
    }
    const parent = dirname(dir);
    if (parent === dir) break;
    dir = parent;
  }
  return resolve(__dirname, '..', '..');
}

/**
 * Read package.json version from the CLI package.
 * @returns {string}
 */
function readVersion() {
  try {
    const pkgPath = join(__dirname, '..', '..', '..', 'package.json');
    const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
    return pkg.version || 'unknown';
  } catch {
    return 'unknown';
  }
}

/**
 * Resolve the skills directory path.
 * @returns {string}
 */
function skillsDir() {
  const root = findRepoRoot();
  const legacy = join(root, 'skills');
  if (existsSync(legacy)) return legacy;
  return join(root, 'src', 'resources', 'skills');
}

// ---------------------------------------------------------------------------
// Memory-backed handlers (8)
// ---------------------------------------------------------------------------

/**
 * Search cross-session memory.
 * @param {object} args
 * @param {string} args.query
 * @param {string} [args.engagement]
 * @param {number} [args.limit]
 * @returns {Promise<object>}
 */
export async function handleSearch(args = {}) {
  const query = getArg(args, "query");
  if (!query) {
    return { error: true, message: 'Usage: nerf search <query> [--engagement <id>] [--limit <n>]' };
  }
  const { AdaptiveRetriever, NerfMemory } = await import('../memory/index.js');
  const memory = new NerfMemory(defaultMemoryDir());
  try {
    const retriever = new AdaptiveRetriever(memory);
    const results = retriever.retrieve(
      query,
      getFlagArg(args, "engagement", "--engagement", ""),
      getFlagArg(args, "limit", "--limit", 10),
    );
    debug(`search: ${results.length} results for "${query}"`);
    return {
      query,
      count: results.length,
      results: results.map(r => ({
        content: r.content,
        type: r.memoryType || '',
        severity: r.severity || '',
        targets: r.targets || [],
        mitre: r.mitreAttack || [],
        created: r.createdAt || '',
      })),
    };
  } finally {
    memory.close();
  }
}

/**
 * Store finding/IOC/TTP in memory.
 * @param {object} args
 * @param {string} args.content
 * @param {string} [args.type]
 * @param {string} [args.severity]
 * @param {string} [args.engagement]
 * @param {string[]} [args.tags]
 * @returns {Promise<object>}
 */
export async function handleStore(args = {}) {
  const content = getArg(args, 'content', '--content');
  if (!content) {
    return { error: true, message: 'Usage: nerf store --content "finding text" [--type finding|ioc|ttp|note]' };
  }
  const { NerfMemory, MemoryEntry, MemoryType } = await import('../memory/index.js');

  // Sanitize content before storage (defense-in-depth against prompt injection)
  let sanitizedContent = content;
  let redactions = [];
  try {
    const { sanitizeContent, sanitizeTags } = await import('../memory/sanitizer.js');
    const result = sanitizeContent(String(content));
    sanitizedContent = result.sanitized;
    redactions = result.redactions;
  } catch {
    // Sanitizer not available — store as-is (existing behavior)
  }

  const memory = new NerfMemory(defaultMemoryDir());
  try {
    const typeStr = getFlagArg(args, 'type', '--type', 'note');
    const rawTags = getFlagArg(args, 'tags', '--tags', '');
    let tags = Array.isArray(rawTags)
      ? rawTags
      : rawTags ? rawTags.split(',').map(t => t.trim()).filter(Boolean) : [];

    // Sanitize tags
    try {
      const { sanitizeTags } = await import('../memory/sanitizer.js');
      tags = sanitizeTags(tags);
    } catch {
      // Sanitizer not available — use tags as-is
    }

    const entry = new MemoryEntry({
      content: sanitizedContent,
      memoryType: Object.values(MemoryType).includes(typeStr) ? typeStr : MemoryType.NOTE,
      severity: getFlagArg(args, 'severity', '--severity', ''),
      engagementId: getFlagArg(args, 'engagement', '--engagement', ''),
      tags,
    });
    const entryId = memory.store(entry);
    debug(`store: id=${entryId} type=${typeStr}${redactions.length ? ` redactions=${redactions.length}` : ''}`);
    return { id: entryId, type: typeStr, stored: true, redactions: redactions.length };
  } finally {
    memory.close();
  }
}

/**
 * Show memory and platform statistics.
 * @returns {Promise<object>}
 */
export async function handleStats(args = {}) {
  const { NerfMemory } = await import('../memory/index.js');
  const memory = new NerfMemory(defaultMemoryDir());
  try {
    const memStats = memory.stats();
    const version = readVersion();
    const sDir = skillsDir();
    let skillCount = 0;
    let domainCount = 0;
    let scriptCount = 0;

    if (existsSync(sDir)) {
      try {
        // Count SKILL.md files recursively
        skillCount = countFiles(sDir, 'SKILL.md');
        // Count top-level domain directories
        domainCount = readdirSync(sDir).filter(d => {
          try {
            return !d.startsWith('.') && statSync(join(sDir, d)).isDirectory();
          } catch { return false; }
        }).length;
        // Count script files recursively
        scriptCount = countFiles(sDir, '*.js', 'scripts');
      } catch {
        // Skills dir unreadable
      }
    }

    debug(`stats: version=${version} memory=${JSON.stringify(memStats)}`);
    return {
      version,
      memory: memStats,
      skills: { total: skillCount, domains: domainCount, scripts: scriptCount },
    };
  } finally {
    memory.close();
  }
}

/**
 * Score response quality (PRM evaluation).
 * @param {object} args
 * @param {string} args.query
 * @param {string} args.response
 * @param {string} [args.mode]
 * @returns {Promise<object>}
 */
export async function handleScore(args = {}) {
  const query = getFlagArg(args, 'query', '--query', '');
  const response = getFlagArg(args, 'response', '--response', '');
  if (!query || !response) {
    return { error: true, message: 'Usage: nerf score --query "question" --response "answer"' };
  }
  const { ResponseScorer } = await import('../memory/index.js');
  const scorer = new ResponseScorer();
  const scored = scorer.score(query, response, getFlagArg(args, 'mode', '--mode', ''));
  debug(`score: ${scored.score}`);
  return {
    score: scored.score,
    votes: scored.votes,
    feedback: scored.feedback,
  };
}

/**
 * Export memory database to JSON.
 * @param {object} args
 * @param {string} [args.output]
 * @param {string} [args.engagement]
 * @returns {Promise<object>}
 */
export async function handleMemoryExport(args = {}) {
  const { NerfMemory } = await import('../memory/index.js');
  const memory = new NerfMemory(defaultMemoryDir());
  try {
    // Query all entries directly — memory.search('') returns nothing for empty queries
    const rows = memory.symbolic.db.prepare(
      'SELECT * FROM entries WHERE is_archived = 0 ORDER BY created_at DESC LIMIT 10000'
    ).all();
    let entries = rows.map(r => {
      let mitre_attack = [], targets = [], tags = [];
      try { mitre_attack = r.mitre_attack ? JSON.parse(r.mitre_attack) : []; } catch { /* corrupt entry */ }
      try { targets = r.targets ? JSON.parse(r.targets) : []; } catch { /* corrupt entry */ }
      try { tags = r.tags ? JSON.parse(r.tags) : []; } catch { /* corrupt entry */ }
      return {
        entry_id: r.entry_id,
        content: r.content,
        memory_type: r.memory_type,
        severity: r.severity,
        engagement_id: r.engagement_id,
        mitre_attack,
        targets,
        tags,
        created_at: r.created_at,
      };
    });
    if (args.engagement) {
      entries = entries.filter(e => e.engagement_id === args.engagement);
    }
    const output = getFlagArg(args, 'output', '--output', 'nerf-memory-export.json');
    const data = { entries, count: entries.length };
    writeFileSync(output, JSON.stringify(data, null, 2), 'utf-8');
    debug(`memory-export: ${entries.length} entries → ${output}`);
    return { exported: entries.length, file: output };
  } finally {
    memory.close();
  }
}

/**
 * Import memory from JSON backup.
 * @param {object} args
 * @param {string} args.file
 * @returns {Promise<object>}
 */
export async function handleMemoryImport(args = {}) {
  const file = getArg(args, 'file');
  if (!file) {
    return { error: true, message: 'Usage: nerf memory-import <file.json>\n\nImport findings, IOCs, and notes from a previously exported JSON file.' };
  }
  if (!existsSync(file)) {
    return { error: true, message: `File not found: ${file}` };
  }
  const { NerfMemory, MemoryEntry } = await import('../memory/index.js');
  const memory = new NerfMemory(defaultMemoryDir());
  try {
    const raw = JSON.parse(readFileSync(file, 'utf-8'));
    const entries = raw.entries || (Array.isArray(raw) ? raw : []);
    let imported = 0;
    for (const entryData of entries) {
      try {
        const entry = MemoryEntry.fromDict(entryData);
        memory.store(entry);
        imported++;
      } catch {
        // Skip invalid entries
      }
    }
    debug(`memory-import: ${imported}/${entries.length} from ${file}`);
    return { imported, total: entries.length, file };
  } finally {
    memory.close();
  }
}

/**
 * Trigger memory ingestion pipeline.
 * @returns {Promise<object>}
 */
export async function handleIngest(args = {}) {
  const { NerfMemory } = await import('../memory/index.js');
  const memory = new NerfMemory(defaultMemoryDir());
  try {
    // Rebuild FTS5 index to fix stale/corrupt indexes
    memory.symbolic.db.exec("INSERT INTO entries_fts(entries_fts) VALUES('rebuild')");
    const stats = memory.consolidate();
    debug('ingest: FTS rebuild + consolidation complete');
    return { ingested: true, fts_rebuilt: true, ...stats };
  } finally {
    memory.close();
  }
}

/**
 * Config check + memory stats + backend reachability.
 * @returns {Promise<object>}
 */
export async function handleStatus(args = {}) {
  const { loadConfig, configExists } = await import('../config.js');
  const { validateConfig } = await import('./config-validate.js');

  const hasConfig = configExists();
  let configValid = false;
  let backend = 'unknown';

  if (hasConfig) {
    try {
      const raw = loadConfig();
      const config = validateConfig(raw);
      configValid = true;
      backend = config.backend;
    } catch {
      // Config present but invalid
    }
  }

  // Memory stats
  let memStats = null;
  try {
    const { NerfMemory } = await import('../memory/index.js');
    const memory = new NerfMemory(defaultMemoryDir());
    memStats = memory.stats();
    memory.close();
  } catch {
    // Memory unavailable
  }

  const status = configValid ? 'ready' : (hasConfig ? 'misconfigured' : 'unconfigured');
  debug(`status: ${status} backend=${backend}`);
  return {
    status,
    backend,
    config: { exists: hasConfig, valid: configValid },
    memory: memStats,
  };
}

// ---------------------------------------------------------------------------
// Pipeline-backed handlers (6)
// ---------------------------------------------------------------------------

/**
 * Analyze git diff for security issues.
 * @param {object} args
 * @param {string} args.file - Diff file path or diff text content
 * @returns {Promise<object>}
 */
export async function handleDiff(args = {}) {
  let diffText = getArg(args, 'file');

  // Check for stdin marker '-' — getArg filters it as a flag, so check raw args
  const isStdin = Array.isArray(args) && args.includes('-');

  if (!diffText && !isStdin) {
    return { error: true, message: 'Usage: nerf diff <file.patch>\n       cat changes.diff | nerf diff -' };
  }
  const { SecurityDiffAnalyzer } = await import('../pipeline/index.js');
  const analyzer = new SecurityDiffAnalyzer();

  // Read from stdin when argument is '-'
  if (isStdin) {
    const chunks = [];
    for await (const chunk of process.stdin) {
      chunks.push(chunk);
    }
    diffText = Buffer.concat(chunks).toString('utf-8');
  }
  // If it looks like a file path (no newlines, exists on disk), read it
  else if (diffText && !diffText.includes('\n') && existsSync(diffText)) {
    diffText = readFileSync(diffText, 'utf-8');
  }

  const analysis = analyzer.analyzeDiff(diffText);
  const riskVal = analysis.riskLevel?.value ?? analysis.riskLevel ?? 'info';

  debug(`diff: risk=${riskVal} files=${analysis.filesChanged?.length}`);
  return {
    risk_level: riskVal,
    files_changed: analysis.filesChanged?.length ?? 0,
    auth_changes: analysis.authChanges?.length ?? 0,
    sql_changes: analysis.sqlChanges?.length ?? 0,
    crypto_changes: analysis.cryptoChanges?.length ?? 0,
    secrets_found: analysis.secrets?.length ?? 0,
    has_security_findings: analysis.hasSecurityFindings ?? false,
    summary: analysis.summary || '',
  };
}

/**
 * Generate GitHub Actions security workflow.
 * @param {object} args
 * @param {string} [args.profile]
 * @returns {Promise<object>}
 */
export async function handleWorkflow(args = {}) {
  const { WorkflowGenerator } = await import('../pipeline/index.js');
  const gen = new WorkflowGenerator();
  const workflow = gen.generateWorkflow(args.profile || 'pentest');
  debug('workflow: generated');
  return { workflow };
}

/**
 * Convert scan results to SARIF v2.1.0.
 * @param {object} args
 * @param {string} args.input - Scan results JSON file path
 * @param {string} [args.output]
 * @returns {Promise<object>}
 */
export async function handleSarif(args = {}) {
  const input = getArg(args, 'input');
  if (!input) {
    return { error: true, message: 'Usage: nerf sarif <scan-results.json> [--output results.sarif]\n\nConvert scan findings to SARIF v2.1.0 for GitHub Advanced Security / CI pipelines.' };
  }
  if (!existsSync(input)) {
    return { error: true, message: `File not found: ${input}` };
  }
  const { SarifReport } = await import('../pipeline/index.js');

  let data;
  try {
    data = JSON.parse(readFileSync(input, 'utf-8'));
  } catch (err) {
    return { error: true, message: `Failed to parse ${input}: ${err.message}` };
  }
  const report = new SarifReport();
  const findings = data.findings || (Array.isArray(data) ? data : []);

  for (const finding of findings) {
    report.addFinding(finding);
  }

  const output = args.output || 'nerf-results.sarif';
  report.write(output);
  const summary = report.summary();

  debug(`sarif: ${findings.length} findings → ${output}`);
  return { file: output, findings: findings.length, summary };
}

/**
 * Run OSINT investigation (domain/IP).
 * @param {object} args
 * @param {string} args.target
 * @param {string} [args.type]
 * @returns {Promise<object>}
 */
export async function handleOsint(args = {}) {
  const target = getArg(args, 'target');
  if (!target) {
    return { error: true, message: 'Usage: nerf osint <target> [--type domain|ip|username|email|url]\n\nInvestigation types:\n  domain    DNS, WHOIS, cert transparency, Wayback Machine, web tech, IP geolocation\n  ip        Reverse DNS, IP classification, geolocation\n  username  Search 400+ platforms via Sherlock\n  email     Check account existence on 120+ services via Holehe\n  url       Wayback Machine + archive.today snapshots' };
  }
  const { OSINTPipeline } = await import('../pipeline/index.js');
  const pipeline = new OSINTPipeline();
  const type = getFlagArg(args, 'type', '--type', null);

  // Auto-detect type if not specified
  let investigationType = type;
  if (!investigationType) {
    if (target.includes('@')) investigationType = 'email';
    else if (target.startsWith('http://') || target.startsWith('https://')) investigationType = 'url';
    else if (/^\d+\.\d+\.\d+\.\d+$/.test(target) || (target.includes(':') && !target.includes('/'))) investigationType = 'ip';
    else if (target.includes('/')) investigationType = 'url';
    else if (target.includes('.')) investigationType = 'domain';
    else investigationType = 'username';
  }

  let results;
  switch (investigationType) {
    case 'ip':
      results = await pipeline.investigateIp(target);
      break;
    case 'username':
      results = pipeline.investigateUsername(target);
      break;
    case 'email':
      results = pipeline.investigateEmail(target);
      break;
    case 'url':
      results = await pipeline.investigateUrl(target);
      break;
    case 'domain':
    default:
      results = await pipeline.investigateDomain(target);
      break;
  }

  const data = {
    target,
    type: investigationType,
    results: results.map(r => (typeof r.toDict === 'function' ? r.toDict() : r)),
    summary: pipeline.summary(),
  };
  debug(`osint: ${target} (${investigationType}) → ${results.length} results`);
  return data;
}

/**
 * List all skill domains.
 * @returns {Promise<object>}
 */
export async function handleDomains(args = {}) {
  const sDir = skillsDir();
  if (!existsSync(sDir)) {
    return { domains: {}, total_domains: 0, total_techniques: 0 };
  }

  const domainMap = {};
  const entries = readdirSync(sDir).sort();
  for (const d of entries) {
    const dirPath = join(sDir, d);
    try {
      if (d.startsWith('.') || !statSync(dirPath).isDirectory()) continue;
    } catch { continue; }

    const techniquesDir = join(dirPath, 'techniques');
    if (existsSync(techniquesDir)) {
      try {
        const techniques = readdirSync(techniquesDir).filter(t => {
          try {
            return statSync(join(techniquesDir, t)).isDirectory();
          } catch { return false; }
        });
        domainMap[d] = techniques.length;
      } catch {
        domainMap[d] = 0;
      }
    }
  }

  const totalTechniques = Object.values(domainMap).reduce((a, b) => a + b, 0);
  debug(`domains: ${Object.keys(domainMap).length} domains, ${totalTechniques} techniques`);
  return {
    domains: domainMap,
    total_domains: Object.keys(domainMap).length,
    total_techniques: totalTechniques,
  };
}

/**
 * Search skills by keyword.
 * @param {object} args
 * @param {string} args.query
 * @param {number} [args.limit]
 * @returns {Promise<object>}
 */
export async function handleSkills(args = {}) {
  const sDir = skillsDir();
  const query = getArg(args, 'query').toLowerCase();
  const limit = parseInt(getFlagArg(args, 'limit', '--limit', 10), 10);
  const results = [];

  if (!existsSync(sDir)) {
    return { query, count: 0, results: [] };
  }

  // Walk skills dir looking for SKILL.md files
  const walk = (dir, relBase) => {
    if (results.length >= limit) return;
    try {
      const entries = readdirSync(dir);
      for (const entry of entries) {
        if (results.length >= limit) return;
        const fullPath = join(dir, entry);
        try {
          if (statSync(fullPath).isDirectory()) {
            walk(fullPath, join(relBase, entry));
          } else if (entry === 'SKILL.md') {
            const name = dirname(join(relBase, entry)).split(/[/\\]/).pop() || '';
            const rel = dirname(join(relBase, entry));
            if (query === '' || name.toLowerCase().includes(query) || rel.toLowerCase().includes(query)) {
              results.push({ name, path: rel });
            }
          }
        } catch { /* skip unreadable entries */ }
      }
    } catch { /* skip unreadable dirs */ }
  };

  walk(sDir, '');
  debug(`skills: "${query}" → ${results.length} results`);
  return { query, count: results.length, results };
}

// ---------------------------------------------------------------------------
// Self-contained handlers (4)
// ---------------------------------------------------------------------------

/**
 * Read version from package.json.
 * @returns {Promise<object>}
 */
export async function handleVersion(args = {}) {
  return { version: readVersion() };
}

/**
 * Show API key status for all known providers without revealing values.
 *
 * Snapshots env var presence BEFORE loadConfig() runs, because _loadSecret()
 * deletes env vars after reading. Then merges with the config object to
 * determine final key status per provider.
 *
 * @returns {Promise<{providers: Array<{name: string, backend: string, keyStatus: string, modelConfigured: string}>}>}
 */
export async function handleKeys(args = {}) {
  // Provider definitions: name, backend id, env var, config path to key, config path to model
  const providerDefs = [
    { name: 'Anthropic (Claude)', backend: 'claude', envVar: 'ANTHROPIC_API_KEY', configKeyPath: ['claude', 'api_key'], configModelPath: ['claude', 'model'] },
    { name: 'OpenAI', backend: 'openai', envVar: 'OPENAI_API_KEY', configKeyPath: ['openai', 'api_key'], configModelPath: ['openai', 'model'] },
    { name: 'OpenRouter', backend: 'openrouter', envVar: 'OPENROUTER_API_KEY', configKeyPath: ['openrouter', 'api_key'], configModelPath: ['openrouter', 'model'] },
    { name: 'Google (Gemini)', backend: 'google', envVar: 'GOOGLE_API_KEY', configKeyPath: ['google', 'api_key'], configModelPath: ['google', 'model'] },
    { name: 'Groq', backend: 'groq', envVar: 'GROQ_API_KEY', configKeyPath: ['groq', 'api_key'], configModelPath: ['groq', 'model'] },
    { name: 'xAI (Grok)', backend: 'xai', envVar: 'XAI_API_KEY', configKeyPath: ['xai', 'api_key'], configModelPath: ['xai', 'model'] },
    { name: 'Mistral', backend: 'mistral', envVar: 'MISTRAL_API_KEY', configKeyPath: ['mistral', 'api_key'], configModelPath: ['mistral', 'model'] },
    { name: 'AWS Bedrock', backend: 'bedrock', envVar: 'AWS_ACCESS_KEY_ID', configKeyPath: ['bedrock', 'access_key_id'], configModelPath: ['bedrock', 'model'] },
    { name: 'Azure OpenAI', backend: 'azure', envVar: 'AZURE_OPENAI_API_KEY', configKeyPath: ['azure', 'api_key'], configModelPath: ['azure', 'model'] },
    { name: 'HuggingFace', backend: 'huggingface', envVar: 'HUGGINGFACE_API_KEY', configKeyPath: ['huggingface', 'api_key'], configModelPath: ['huggingface', 'model'] },
    { name: 'Ollama (local)', backend: 'ollama', envVar: null, configKeyPath: null, configModelPath: ['ollama', 'model'] },
    { name: 'OpenAI-Compatible', backend: 'openai-compat', envVar: null, configKeyPath: ['openai_compat', 'api_key'], configModelPath: ['openai_compat', 'model'] },
    { name: 'Pi AI', backend: 'pi-ai', envVar: null, configKeyPath: null, configModelPath: ['pi_ai', 'model'] },
  ];

  // Snapshot env var presence BEFORE loadConfig (which deletes them via _loadSecret)
  const envSnapshot = {};
  for (const def of providerDefs) {
    if (def.envVar) {
      envSnapshot[def.envVar] = !!process.env[def.envVar];
    }
  }

  // Load config (this may delete env vars)
  let config;
  try {
    const { loadConfig } = await import('../config.js');
    config = loadConfig();
  } catch {
    config = {};
  }

  const activeBackend = config.llm_backend || '';

  /**
   * Safely traverse nested config object.
   * @param {string[]|null} path
   * @returns {string}
   */
  function getConfigValue(path) {
    if (!path) return '';
    let obj = config;
    for (const key of path) {
      if (!obj || typeof obj !== 'object') return '';
      obj = obj[key];
    }
    return (typeof obj === 'string') ? obj : '';
  }

  /**
   * Check if a Docker secret exists for a provider.
   * @param {string} envVar
   * @returns {boolean}
   */
  function hasDockerSecret(envVar) {
    if (!envVar) return false;
    const secretName = envVar.toLowerCase();
    try {
      return existsSync(`/run/secrets/${secretName}`);
    } catch {
      return false;
    }
  }

  const providers = providerDefs.map(def => {
    // Determine key status
    let keyStatus = 'missing';

    if (def.envVar && envSnapshot[def.envVar]) {
      keyStatus = 'env-var';
    } else if (def.envVar && hasDockerSecret(def.envVar)) {
      keyStatus = 'docker-secret';
    } else if (def.configKeyPath && getConfigValue(def.configKeyPath)) {
      keyStatus = 'config-file';
    } else if (!def.envVar && !def.configKeyPath) {
      // Providers like Ollama/Pi that don't need API keys
      keyStatus = 'not-required';
    }

    const model = getConfigValue(def.configModelPath);

    return {
      name: def.name,
      backend: def.backend,
      keyStatus,
      modelConfigured: model || '',
      isActive: activeBackend === def.backend,
    };
  });

  return { providers, activeBackend };
}

/**
 * Comprehensive health check.
 * @returns {Promise<object>}
 */
export async function handleDoctor(args = {}) {
  const checks = [];
  const home = homedir();

  // Extra search paths for tools not in system PATH (Go, pipx, cargo, etc.)
  const extraPaths = [
    `${home}/go/bin`,
    `${home}/.local/bin`,
    `${home}/.cargo/bin`,
    `${home}/.lmstudio/bin`,
    '/usr/local/go/bin',
  ].filter(p => { try { return statSync(p).isDirectory(); } catch { return false; } });

  /**
   * Check if a binary exists in PATH or common extra locations.
   * @param {string} binary
   * @returns {boolean}
   */
  function findBinary(binary) {
    try {
      execFileSync('which', [binary], { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
      return true;
    } catch {
      // Check extra paths
      for (const dir of extraPaths) {
        if (existsSync(join(dir, binary))) return true;
      }
      return false;
    }
  }

  // Node.js version
  checks.push({
    name: 'Node.js',
    status: 'ok',
    detail: process.version,
  });

  // npm version
  try {
    const npmVer = execSync('npm --version', { encoding: 'utf-8', timeout: 5000 }).trim();
    checks.push({ name: 'npm', status: 'ok', detail: `v${npmVer}` });
  } catch {
    checks.push({ name: 'npm', status: 'missing', detail: 'npm not found' });
  }

  // External tools — organized by mode/capability
  // Core tools (required for basic operation)
  const coreTools = [
    { name: 'git', install: 'pacman -S git / apt install git' },
  ];

  // Scanning tools (RED/BLUE/PURPLE modes)
  const scanTools = [
    { name: 'nuclei', install: 'go install github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest' },
    { name: 'katana', install: 'go install github.com/projectdiscovery/katana/cmd/katana@latest' },
    { name: 'subfinder', install: 'go install github.com/projectdiscovery/subfinder/v2/cmd/subfinder@latest' },
    { name: 'httpx', install: 'go install github.com/projectdiscovery/httpx/cmd/httpx@latest' },
    { name: 'nmap', install: 'pacman -S nmap / apt install nmap' },
  ];

  // Red team / offensive tools (RED mode)
  const redTeamTools = [
    { name: 'sqlmap', install: 'pacman -S sqlmap / apt install sqlmap' },
    { name: 'ffuf', install: 'go install github.com/ffuf/ffuf/v2@latest' },
    { name: 'gobuster', install: 'go install github.com/OJ/gobuster/v3@latest' },
    { name: 'hydra', install: 'pacman -S hydra / apt install hydra' },
    { name: 'metasploit', install: 'https://docs.metasploit.com/docs/using-metasploit/getting-started/nightly-installers.html', bin: 'msfconsole' },
    { name: 'sliver', install: 'https://github.com/BishopFox/sliver/wiki/Getting-Started' },
    { name: 'john', install: 'pacman -S john / apt install john' },
    { name: 'hashcat', install: 'pacman -S hashcat / apt install hashcat' },
  ];

  // Blue/Purple team tools (BLUE/PURPLE modes)
  const blueTeamTools = [
    { name: 'sigma', install: 'pipx install sigma-cli', bin: 'sigma' },
    { name: 'caldera', install: 'https://github.com/mitre/caldera#installation' },
    { name: 'bloodhound', install: 'https://github.com/BloodHoundAD/BloodHound/releases', bin: 'bloodhound' },
    { name: 'wireshark', install: 'pacman -S wireshark-qt / apt install wireshark', bin: 'wireshark' },
    { name: 'tshark', install: 'pacman -S wireshark-cli / apt install tshark' },
  ];

  // OSINT tools (RECON mode)
  const osintTools = [
    { name: 'whois', install: 'pacman -S whois / apt install whois' },
    { name: 'dig', install: 'pacman -S bind / apt install dnsutils' },
    { name: 'sherlock', install: 'pipx install sherlock-project' },
    { name: 'holehe', install: 'pipx install holehe' },
    { name: 'exiftool', install: 'pacman -S perl-image-exiftool / apt install libimage-exiftool-perl' },
    { name: 'amass', install: 'go install github.com/owasp-amass/amass/v4/...@master' },
  ];

  // Forensics / reverse engineering tools (INCIDENT mode)
  const forensicsTools = [
    { name: 'volatility', install: 'pip install volatility3', bin: 'vol' },
    { name: 'yara', install: 'pacman -S yara / apt install yara' },
    { name: 'strings', install: 'pacman -S binutils / apt install binutils' },
    { name: 'objdump', install: 'pacman -S binutils / apt install binutils' },
    { name: 'binwalk', install: 'pacman -S binwalk / apt install binwalk' },
    { name: 'radare2', install: 'pacman -S radare2 / apt install radare2', bin: 'r2' },
    { name: 'ghidra', install: 'pacman -S ghidra / https://ghidra-sre.org', bin: 'ghidra' },
    { name: 'gdb', install: 'pacman -S gdb / apt install gdb' },
  ];

  // Network tools (RED/BLUE modes)
  const networkTools = [
    { name: 'curl', install: 'pacman -S curl / apt install curl' },
    { name: 'masscan', install: 'pacman -S masscan / apt install masscan' },
    { name: 'nikto', install: 'pacman -S nikto / apt install nikto' },
    { name: 'tcpdump', install: 'pacman -S tcpdump / apt install tcpdump' },
    { name: 'socat', install: 'pacman -S socat / apt install socat' },
    { name: 'ncat', install: 'pacman -S nmap / apt install ncat' },
  ];

  // Supply chain / DevSecOps tools (BUILD mode)
  const devsecopsTools = [
    { name: 'grype', install: 'https://github.com/anchore/grype#installation' },
    { name: 'syft', install: 'https://github.com/anchore/syft#installation' },
    { name: 'cosign', install: 'go install github.com/sigstore/cosign/v2/cmd/cosign@latest' },
  ];

  // Report export tools (EXPORT)
  const exportTools = [
    { name: 'pandoc', install: 'pacman -S pandoc / apt install pandoc / brew install pandoc' },
    { name: 'weasyprint', install: 'pip install weasyprint (PDF engine for pandoc)' },
    { name: 'wkhtmltopdf', install: 'pacman -S wkhtmltopdf / apt install wkhtmltopdf (PDF engine for pandoc)' },
  ];

  // Container/sandbox tools (BENCHMARK mode)
  const containerTools = [
    { name: 'docker', install: 'pacman -S docker / apt install docker.io' },
  ];

  // LLM backends
  const llmTools = [
    { name: 'ollama', install: 'https://ollama.com/download' },
    { name: 'lms', install: 'https://lmstudio.ai/download', label: 'LM Studio' },
  ];

  // Active connectivity checks for local LLM servers
  // Ollama
  let ollamaRunning = false;
  try {
    execFileSync('curl', ['-sf', '-o', '/dev/null', '--connect-timeout', '2', 'http://127.0.0.1:11434/v1/models'], { timeout: 4000, stdio: ['pipe', 'pipe', 'pipe'] });
    ollamaRunning = true;
  } catch { /* not running */ }
  checks.push({
    name: 'ollama_server',
    label: 'Ollama Server',
    status: ollamaRunning ? 'ok' : 'warning',
    detail: ollamaRunning ? 'running (port 11434)' : 'not running — start with: ollama serve',
    group: 'LLM Backend',
  });

  // LM Studio server (port 1234 default)
  let lmsRunning = false;
  const lmsPorts = [1234, 11435];
  let lmsPort = 1234;
  for (const p of lmsPorts) {
    try {
      execFileSync('curl', ['-sf', '-o', '/dev/null', '--connect-timeout', '2', `http://127.0.0.1:${p}/v1/models`], { timeout: 4000, stdio: ['pipe', 'pipe', 'pipe'] });
      lmsRunning = true;
      lmsPort = p;
      break;
    } catch { /* try next port */ }
  }
  checks.push({
    name: 'lms_server',
    label: 'LM Studio Server',
    status: lmsRunning ? 'ok' : 'optional',
    detail: lmsRunning
      ? `running (port ${lmsPort})`
      : 'not running — open LM Studio → Local Server → Start Server',
    group: 'LLM Backend',
  });

  // Signal bot integration
  const integrationTools = [
    { name: 'signal-cli-rest-api', install: 'docker run -d -p 8080:8080 bbernhard/signal-cli-rest-api', label: 'Signal API (Docker)', bin: '__docker_check__' },
  ];

  // Check all tool groups
  const toolGroups = [
    { label: 'Core', tools: coreTools },
    { label: 'Scanning (RED/BLUE/PURPLE)', tools: scanTools },
    { label: 'Red Team (RED)', tools: redTeamTools },
    { label: 'Blue/Purple Team (BLUE/PURPLE)', tools: blueTeamTools },
    { label: 'OSINT (RECON)', tools: osintTools },
    { label: 'Forensics & RE (INCIDENT)', tools: forensicsTools },
    { label: 'Network (RED/BLUE)', tools: networkTools },
    { label: 'DevSecOps (BUILD)', tools: devsecopsTools },
    { label: 'Report Export (EXPORT)', tools: exportTools },
    { label: 'Container (BENCHMARK)', tools: containerTools },
    { label: 'LLM Backend', tools: llmTools },
    { label: 'Integrations', tools: integrationTools },
  ];

  for (const group of toolGroups) {
    for (const { name, install, bin, label } of group.tools) {
      const binary = bin || name;

      // Special check: Docker-based services
      if (binary === '__docker_check__') {
        let found = false;
        try {
          // Check if the signal-cli-rest-api container is running
          const out = execSync('docker ps --format "{{.Image}}" 2>/dev/null', { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
          found = out.includes('signal-cli-rest-api');
        } catch { /* docker not available or no containers */ }
        if (!found) {
          // Check if the service URL responds
          try {
            const { loadConfig, configExists } = await import('../config.js');
            if (configExists()) {
              const cfg = loadConfig();
              const svc = cfg.signal?.signal_service || process.env.SIGNAL_SERVICE || '';
              if (svc) {
                execFileSync('curl', ['-sf', '-o', '/dev/null', '--connect-timeout', '2', `${svc}/v1/about`], { timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] });
                found = true;
              }
            }
          } catch { /* service not responding */ }
        }
        checks.push({
          name, label,
          status: found ? 'ok' : 'optional',
          detail: found ? 'running' : `not running (${install})`,
          group: group.label,
        });
        continue;
      }

      if (findBinary(binary)) {
        checks.push({ name, label, status: 'ok', detail: 'installed', group: group.label });
      } else {
        checks.push({ name, label, status: 'optional', detail: `not installed (${install})`, group: group.label });
      }
    }
  }

  // Config validity
  try {
    const { loadConfig, configExists } = await import('../config.js');
    const { validateConfig } = await import('./config-validate.js');

    if (configExists()) {
      const raw = loadConfig();
      const config = validateConfig(raw);
      const modelKey = config.backend === 'openai-compat' ? 'compat_model'
        : config.backend === 'pi-ai' || config.backend === 'pi' ? 'pi_model'
        : `${config.backend}_model`;
      checks.push({
        name: 'Config',
        status: 'ok',
        detail: `backend=${config.backend} model=${config[modelKey] || 'default'}`,
      });
    } else {
      checks.push({
        name: 'Config',
        status: 'missing',
        detail: 'No config found — run: nerf setup',
      });
    }
  } catch (err) {
    checks.push({
      name: 'Config',
      status: 'error',
      detail: err.message?.split('\n')[0] || 'Config validation failed',
    });
  }

  // Memory database health
  try {
    const { NerfMemory } = await import('../memory/index.js');
    const memory = new NerfMemory(defaultMemoryDir());
    const stats = memory.stats();
    memory.close();
    checks.push({
      name: 'Memory',
      status: 'ok',
      detail: `total=${stats.total ?? 0} active=${stats.active ?? 0} archived=${stats.archived ?? 0}`,
    });
  } catch (err) {
    checks.push({
      name: 'Memory',
      status: 'error',
      detail: err.message || 'Memory database unavailable',
    });
  }

  // ── Engine environment checks (graceful degradation if modules unavailable) ──
  try {
    const { runFullEnvironmentChecks } = await import(
      '../../../dist/resources/extensions/nerf-engine/doctor-environment.js'
    );
    const envResults = runFullEnvironmentChecks(process.cwd());
    for (const r of envResults) {
      checks.push({
        name: r.name,
        status: r.status === 'error' ? 'error' : r.status === 'warning' ? 'warning' : 'ok',
        detail: r.detail ? `${r.message} — ${r.detail}` : r.message,
        group: 'Environment',
      });
    }
  } catch (err) {
    debug(`doctor: engine environment checks unavailable: ${err.message}`);
  }

  // ── Engine provider checks (graceful degradation if modules unavailable) ──
  try {
    const { runProviderChecks } = await import(
      '../../../dist/resources/extensions/nerf-engine/doctor-providers.js'
    );
    const providerResults = runProviderChecks();
    const categoryLabels = { llm: 'LLM Providers', remote: 'Notifications', search: 'Search', tool: 'Tools' };
    for (const r of providerResults) {
      checks.push({
        name: r.name,
        label: r.label,
        status: r.status === 'error' ? 'error'
          : r.status === 'warning' ? 'warning'
          : r.status === 'unconfigured' ? 'optional'
          : 'ok',
        detail: r.detail ? `${r.message} — ${r.detail}` : r.message,
        group: categoryLabels[r.category] || r.category,
      });
    }
  } catch (err) {
    debug(`doctor: engine provider checks unavailable: ${err.message}`);
  }

  const coreChecks = checks.filter(c => !c.group);
  const healthy = coreChecks.every(c => c.status === 'ok' || c.status === 'optional');
  const installedTools = checks.filter(c => c.group && c.status === 'ok').length;
  const totalTools = checks.filter(c => c.group).length;
  const engineChecks = checks.filter(c => c.group === 'Environment' || c.group === 'LLM Providers' || c.group === 'Notifications' || c.group === 'Search' || c.group === 'Tools');
  const summary = `${coreChecks.filter(c => c.status === 'ok').length}/${coreChecks.length} core checks passed, ${installedTools}/${totalTools} tools installed, ${engineChecks.length} engine checks`;

  debug(`doctor: ${summary}`);
  return { checks, healthy, summary };
}

/**
 * Plugin management.
 * @param {object} args
 * @param {string} [args.action] - 'list', 'install', 'remove'
 * @returns {Promise<object>}
 */
export async function handlePlugin(args = {}) {
  const { PluginManager } = await import('./plugins.js');
  const pm = new PluginManager();
  const action = args.action || 'list';

  if (action === 'list') {
    const plugins = pm.plugins;
    return {
      count: plugins.length,
      plugins: plugins.map(p => ({
        name: p.name,
        version: p.version,
        description: p.description,
        author: p.author,
        modes: p.modes,
        priority: p.priority,
        source: p.source,
      })),
    };
  }

  // install/remove are placeholder — real implementation depends on registry
  return {
    error: true,
    message: `Plugin action '${action}' not yet implemented. Only 'list' is available.`,
  };
}

/**
 * Non-streaming query path — delegates to Gateway.send().
 * @param {object} args
 * @param {string} args.message
 * @param {Array<{role: string, content: string}>} [args.history]
 * @returns {Promise<object>}
 */
export async function handleQuery(args = {}) {
  const { Gateway } = await import('./gateway.js');
  const gateway = new Gateway();
  const response = await gateway.send(args.message || '', args.history);
  debug('query: response received');
  return { response, mode: 'auto' };
}

// ---------------------------------------------------------------------------
// Stub handlers (4)
// ---------------------------------------------------------------------------

/**
 * @returns {Promise<object>}
 */
export async function handleLeaderboard(args = {}) {
  try {
    const { SkillLeaderboard } = await import('../execution-engine/leaderboard.js');
    const lb = new SkillLeaderboard();
    const n = args.top || args.n || 20;
    const domain = args.domain || undefined;
    const topSkills = lb.getTopSkills(n, domain);
    return {
      top_skills: topSkills,
      total: topSkills.length,
    };
  } catch (err) {
    return {
      top_skills: [],
      total: 0,
      error: `Leaderboard unavailable: ${err.message}`,
    };
  }
}

/**
 * @returns {Promise<object>}
 */
export async function handleMarketplace(args = {}) {
  try {
    const { SkillMarketplace } = await import('../api/marketplace.js');
    const mp = new SkillMarketplace();
    const action = Array.isArray(args) ? args[0] : args.action;

    if (action === 'search') {
      const query = Array.isArray(args) ? args.find((a, i) => args[i - 1] === '--query') || args[1] || '' : args.query || '';
      const results = mp.search(query);
      mp.close();
      return { query, results: results.map(p => ({ name: p.name, domain: p.domain, rating: p.rating, downloads: p.downloads })), total: results.length };
    }

    const index = mp.getIndex();
    mp.close();
    return index;
  } catch (err) {
    return { error: true, message: `Marketplace error: ${err.message}` };
  }
}

/**
 * @returns {Promise<object>}
 */
export async function handleCompliance(args = {}) {
  try {
    const { ComplianceEngine, ComplianceFramework } = await import('../api/compliance.js');
    const engine = new ComplianceEngine();
    const framework = Array.isArray(args) ? args[0] : args.framework;

    if (!framework) {
      return {
        available_frameworks: Object.keys(ComplianceFramework),
        total: Object.keys(ComplianceFramework).length,
        usage: 'nerf compliance <FRAMEWORK> [--format json|markdown|csv]',
      };
    }

    const fw = framework.toUpperCase().replace(/-/g, '_');
    if (!ComplianceFramework[fw]) {
      return { error: true, message: `Unknown framework: ${framework}. Available: ${Object.keys(ComplianceFramework).join(', ')}` };
    }

    const format = Array.isArray(args) ? (args.find((a, i) => args[i - 1] === '--format') || 'json') : (args.format || 'json');
    const report = engine.assessFromFindings([], fw);

    if (format === 'markdown') {
      return { output: report.toMarkdown() };
    } else if (format === 'csv') {
      return { output: engine.exportCsv(report) };
    }

    return report.toDict();
  } catch (err) {
    return { error: true, message: `Compliance error: ${err.message}` };
  }
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/**
 * Recursively count files matching a filename pattern.
 * @param {string} dir
 * @param {string} filename - exact filename (e.g. 'SKILL.md') or glob-like '*.py'
 * @param {string} [subdir] - optional subdirectory filter (only count in dirs named this)
 * @returns {number}
 */
function countFiles(dir, filename, subdir) {
  let count = 0;
  const isGlob = filename.startsWith('*.');
  const ext = isGlob ? filename.slice(1) : null;

  const walk = (d) => {
    try {
      const entries = readdirSync(d);
      for (const entry of entries) {
        const fullPath = join(d, entry);
        try {
          const st = statSync(fullPath);
          if (st.isDirectory()) {
            if (!subdir || entry === subdir || d !== dir) {
              walk(fullPath);
            }
          } else if (st.isFile()) {
            if (isGlob ? entry.endsWith(ext) : entry === filename) {
              if (!subdir || dirname(fullPath).split(/[/\\]/).includes(subdir)) {
                count++;
              }
            }
          }
        } catch { /* skip unreadable entries */ }
      }
    } catch { /* skip unreadable dirs */ }
  };

  walk(dir);
  return count;
}

// ---------------------------------------------------------------------------
// Scan command — delegates to Node.js NucleiRunner
// ---------------------------------------------------------------------------

export async function handleScan(args = {}) {
  const target = Array.isArray(args) ? args.find(a => !a.startsWith('-')) : args.target;
  if (!target) {
    return { error: true, message: 'Usage: nerf scan <target> [--profile <profile>]\n\nProfiles: quick, standard, pentest, recon, full\n  quick     Tech detection + known CVEs only (~30s)\n  standard  Common vulns + misconfigs (~2-5 min)\n  pentest   Full pentest template set (~5-15 min)\n  recon     Discovery + exposure (~2-5 min)\n  full      All templates (~15-30 min)' };
  }
  try {
    const { NucleiRunner, ScanProfile } = await import('../pipeline/scanner.js');
    const runner = new NucleiRunner();

    if (!runner.available) {
      return { error: true, message: 'nuclei is not installed.\nInstall: go install -v github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest' };
    }

    const profileArg = Array.isArray(args) ? (args.find((a, i) => args[i - 1] === '--profile') || 'standard') : (args.profile || 'standard');

    // Quick profile — fast tech detection + critical CVEs only
    let scanOpts;
    if (profileArg === 'quick') {
      // Quick mode: use specific fast templates instead of tag-based matching
      // This avoids nuclei loading 900+ templates for the 'tech' tag
      const quickTemplates = [
        'http/technologies/tech-detect.yaml',
        'http/technologies/waf-detect.yaml',
        'dns/dns-waf-detect.yaml',
      ];
      // Resolve template paths — check nuclei's default template dir
      const { homedir: hd } = await import('node:os');
      const { existsSync: exists } = await import('node:fs');
      const { join: pjoin } = await import('node:path');
      const templateBase = pjoin(hd(), 'nuclei-templates');
      const resolvedPaths = quickTemplates
        .map(t => pjoin(templateBase, t))
        .filter(t => exists(t));

      if (resolvedPaths.length > 0) {
        scanOpts = { templatePaths: resolvedPaths, timeout: 30 };
      } else {
        // Fallback: use tags if template dir not found
        scanOpts = {
          profile: { tags: ['tech'], severity: ['info'], rateLimit: 150, bulkSize: 50, concurrency: 50, requestTimeout: 5, headless: false },
          timeout: 120,
        };
      }
      process.stderr.write('  ● Scanning (quick — tech detection, ~5-10s)...\n');
    } else {
      scanOpts = { profile: ScanProfile.fromDomain(profileArg) };
      process.stderr.write(`  ● Scanning (${profileArg} profile — this may take several minutes)...\n`);
    }

    const result = scanOpts.templatePaths
      ? await runner.scanWithTemplates(target, scanOpts)
      : await runner.scan(target, scanOpts);

    process.stderr.write(`  ✔ Scan complete: ${(result.findings || []).length} finding(s) in ${result.durationSeconds || '?'}s\n`);

    return {
      output: JSON.stringify({
        target,
        profile: profileArg,
        findings_count: (result.findings || []).length,
        findings: (result.findings || []).map(f => ({
          name: f.name || f.templateId,
          severity: f.severity,
          matched_at: f.matchedAt || f.host,
          template: f.templateId,
          description: f.description || '',
        })),
        stats: result.stats || {},
        duration_seconds: result.durationSeconds || 0,
        status: result.success ? 'completed' : 'completed_with_errors',
        errors: (result.errors || []).length > 0 ? result.errors : undefined,
      }, null, 2),
    };
  } catch (err) {
    return { error: true, message: `Scan failed: ${err.message}` };
  }
}

// ---------------------------------------------------------------------------
// Setup Signal command — Signal bot configuration
// ---------------------------------------------------------------------------

export async function handleSetupSignal() {
  const clack = await import('@clack/prompts');
  const { execSync } = await import('node:child_process');
  const { writeConfig, loadConfig, configExists } = await import('../config.js');

  clack.intro('Signal Bot Setup');

  // Step 1: Check if signal-cli-rest-api is running
  let signalService = 'http://localhost:8080';
  let apiHealthy = false;

  clack.log.step('Checking for signal-cli-rest-api...');

  try {
    const resp = execFileSync('curl', ['-sf', `${signalService}/v1/about`], { encoding: 'utf-8', timeout: 5000 });
    const info = JSON.parse(resp);
    clack.log.success(`Signal API running (v${info.version}, mode: ${info.mode})`);
    apiHealthy = true;
  } catch {
    clack.log.warn('Signal API not detected at localhost:8080');
    clack.log.info(
      'Start it with Docker:\n\n' +
      '  docker compose --profile signal up -d signal-api\n\n' +
      'Or manually:\n\n' +
      '  docker run -d --name signal-api \\\n' +
      '    -p 8080:8080 -e MODE=json-rpc \\\n' +
      '    -v signal-data:/home/.local/share/signal-cli \\\n' +
      '    bbernhard/signal-cli-rest-api:0.98'
    );

    const customUrl = await clack.text({
      message: 'Signal API URL (or press Enter to skip)',
      placeholder: 'http://localhost:8080',
      defaultValue: '',
    });
    if (clack.isCancel(customUrl)) { clack.outro('Cancelled.'); return {}; }

    if (customUrl) {
      signalService = customUrl;
      try {
        const resp = execFileSync('curl', ['-sf', `${signalService}/v1/about`], { encoding: 'utf-8', timeout: 5000 });
        clack.log.success('Signal API reachable.');
        apiHealthy = true;
      } catch {
        clack.log.error('Cannot reach Signal API at that URL.');
        clack.log.warn("Fix the Signal API first, then run nerf setup-signal again."); return {};
      }
    } else {
      clack.log.warn("Start the Signal API first, then run nerf setup-signal again."); return {};
    }
  }

  // Step 2: Check registered phone numbers
  let phoneNumber = '';
  try {
    const accounts = JSON.parse(execFileSync('curl', ['-sf', `${signalService}/v1/accounts`], { encoding: 'utf-8', timeout: 5000 }));
    if (accounts.length > 0) {
      if (accounts.length === 1) {
        phoneNumber = accounts[0];
        clack.log.success(`Registered number: ${phoneNumber}`);
      } else {
        const selected = await clack.select({
          message: 'Select the phone number for the bot',
          options: accounts.map(a => ({ value: a, label: a })),
        });
        if (clack.isCancel(selected)) { clack.outro('Cancelled.'); return {}; }
        phoneNumber = selected;
      }
    } else {
      clack.log.warn('No phone numbers registered with signal-cli.');
      clack.log.info(
        'Register a number:\n\n' +
        `  curl -X POST ${signalService}/v1/register/<YOUR_NUMBER>\n` +
        `  curl -X POST ${signalService}/v1/verify/<YOUR_NUMBER> -d '{"token":"<CODE>"}'`
      );
      clack.log.warn("Register a number first, then run nerf setup-signal again."); return {};
    }
  } catch (err) {
    clack.log.error('Failed to query accounts: ' + err.message);
    return { error: true, message: err.message };
  }

  // Step 3: Whitelist
  const whitelistInput = await clack.text({
    message: 'Allowed phone numbers (comma-separated, E.164 format)',
    placeholder: '+15551234567,+15559876543',
    defaultValue: '',
  });
  if (clack.isCancel(whitelistInput)) { clack.outro('Cancelled.'); return {}; }

  const whitelist = whitelistInput
    ? whitelistInput.split(',').map(n => n.trim()).filter(Boolean)
    : [];

  // Step 4: Secrets management method — only ask if backend needs an API key
  let currentBackend = 'claude';
  try {
    const { loadConfig: lc, configExists: ce } = await import('../config.js');
    if (ce()) currentBackend = lc().llm_backend || 'claude';
  } catch {}

  const needsApiKey = ['claude', 'openai', 'openrouter', 'api:anthropic', 'api:openai'].includes(currentBackend)
    || currentBackend === 'anthropic';
  const localBackend = ['ollama', 'openai-compat', 'lmstudio', 'pi-ai', 'pi'].includes(currentBackend);

  let secretsMethod;
  let opReference = '';

  if (localBackend) {
    // Local backends don't need API key management for the bot
    clack.log.info(`Backend: ${currentBackend} (local — no API key needed for bot)`);
  } else {
    const keyName = currentBackend === 'openai' ? 'OPENAI_API_KEY' : 'ANTHROPIC_API_KEY';
    secretsMethod = await clack.select({
      message: `How should the bot load the ${keyName}?`,
      options: [
        { value: 'op', label: '1Password CLI (op run)', hint: 'Recommended — secrets never touch disk' },
        { value: 'env', label: '.env file', hint: `Simpler — ${keyName} stored in .env (gitignored)` },
        { value: 'manual', label: 'Environment variable', hint: `You export ${keyName} yourself` },
      ],
    });
    if (clack.isCancel(secretsMethod)) { clack.outro('Cancelled.'); return {}; }

    if (secretsMethod === 'op') {
      try {
        execSync('op --version', { encoding: 'utf-8', timeout: 5000 });
      } catch {
        clack.log.warn('1Password CLI (op) not found in PATH.');
        clack.log.info('Install: https://developer.1password.com/docs/cli/get-started/');
      }

      const ref = await clack.text({
        message: `1Password secret reference for ${keyName}`,
        placeholder: 'op://Vault/Item/field',
        defaultValue: '',
      });
      if (clack.isCancel(ref)) { clack.outro('Cancelled.'); return {}; }
      opReference = ref || '';

      if (opReference) {
        clack.log.info(
          'Start the bot with:\n\n' +
          `  ${keyName}=$(op read "${opReference}") nerf bot\n\n` +
          'Or use the helper script:\n\n' +
          '  ./scripts/signal-bot-start.sh --standalone'
        );
      }
    } else if (secretsMethod === 'env') {
      clack.log.info(
        `Add your API key to .env (gitignored):\n\n` +
        `  echo "${keyName}=your-key-here" >> .env\n\n` +
        'Start with:\n\n' +
        '  source .env && nerf bot'
      );
    }
  }

  // Step 5: Save config
  let existingConfig = {};
  try {
    if (configExists()) existingConfig = loadConfig();
  } catch { /* ignore */ }

  const config = {
    ...existingConfig,
    signal: {
      signal_service: signalService,
      phone_number: phoneNumber,
      whitelist,
      session_timeout: 3600,
      ...(secretsMethod ? { secrets_method: secretsMethod } : {}),
      ...(opReference ? { op_reference: opReference } : {}),
    },
  };

  const configPath = writeConfig(config);

  clack.log.success('Signal configuration saved.');
  clack.outro(`Run nerf bot to start. Config: ${configPath}`);
  return {};
}

// ---------------------------------------------------------------------------
// Update command — self-update from npm
// ---------------------------------------------------------------------------

export async function handleUpdate(args = {}) {
  const { execSync } = await import('node:child_process');
  const { readFileSync } = await import('node:fs');
  const { join, dirname } = await import('node:path');
  const brand = await import('../brand.js');

  const pkgPath = join(dirname(new URL(import.meta.url).pathname), '..', '..', '..', 'package.json');
  let currentVersion = 'unknown';
  try {
    currentVersion = JSON.parse(readFileSync(pkgPath, 'utf8')).version;
  } catch { /* ignore */ }

  brand.header('Update', currentVersion);
  brand.info(`Current version: ${currentVersion}`);
  brand.info('Checking npm for updates...');

  try {
    const latest = execSync('npm view @defconxt/nerf version', {
      encoding: 'utf-8',
      timeout: 10000,
    }).trim();

    if (latest === currentVersion) {
      brand.success(`Already up to date (${currentVersion})`);
      return {};
    }

    brand.warn(`New version available: ${latest}`);
    brand.info('Installing update...');

    execSync('npm install -g @defconxt/nerf@latest 2>&1', {
      encoding: 'utf-8',
      timeout: 60000,
      stdio: 'pipe',
    });

    brand.success(`Updated ${currentVersion} → ${latest}`);
    return {};
  } catch (err) {
    brand.error(`Update failed: ${err.message}`);
    return { error: true, message: `Update failed: ${err.message}` };
  }
}

// ---------------------------------------------------------------------------
// Review — Multi-Layer Code Review Engine
// ---------------------------------------------------------------------------

/**
 * nerf review <target> [--format json|text] [--min-severity critical|high|medium|low|info]
 *
 * Run 3-layer parallel code review: Blind Hunter (pattern-based vuln detection),
 * Edge Case Hunter (boundary/failure analysis), Acceptance Auditor (security architecture).
 *
 * @param {string[]|object} args
 * @returns {Promise<object>}
 */
export async function handleReview(args = {}) {
  const argList = Array.isArray(args) ? args : [args.target].filter(Boolean);
  const target = argList.find((a) => !a.startsWith('-'));

  if (!target) {
    return {
      error: true,
      message: [
        'Usage: nerf review <file|dir|code> [options]',
        '',
        'Options:',
        '  --format <text|json>      Output format (default: text)',
        '  --min-severity <level>    Filter: critical, high, medium, low, info',
        '  --single-pass             Use single-pass engine (skip two-stage pipeline)',
        '  --budget                  Show token budget allocation',
        '',
        'Default mode runs a two-stage review pipeline:',
        '  Stage 1: Spec Compliance — security requirements gate',
        '  Stage 2: Quality Gate    — code quality patterns (runs if Stage 1 passes)',
        '',
        'Single-pass mode (--single-pass) runs 3 parallel review layers:',
        '  • Blind Hunter       — Pattern-based vulnerability detection',
        '  • Edge Case Hunter   — Boundary condition & failure analysis',
        '  • Acceptance Auditor — Security architecture review',
      ].join('\n'),
    };
  }

  const format = argList.find((a, i) => argList[i - 1] === '--format') || 'text';
  const minSeverity = argList.find((a, i) => argList[i - 1] === '--min-severity');
  const singlePass = argList.includes('--single-pass');
  const showBudget = argList.includes('--budget');

  // Single-pass mode: preserves original createReviewEngine behavior
  if (singlePass) {
    try {
      const { createReviewEngine } = await import('../review/engine.js');
      const engine = await createReviewEngine();
      const result = await engine.review(target, { minSeverity });

      if (format === 'json') {
        return result.toJSON();
      }
      return { output: result.toReport() };
    } catch (err) {
      return { error: true, message: `Review failed: ${err.message}` };
    }
  }

  // Default: two-stage pipeline with optional budget
  try {
    const { twoStageReview } = await import('../review/two-stage.js');
    const { createTokenBudget } = await import('../review/budget.js');

    const reviewOptions = {};
    if (minSeverity) {
      reviewOptions.compliance = { minSeverity };
      reviewOptions.quality = { minSeverity };
    }

    const [reviewResult, budget] = await Promise.all([
      twoStageReview(target, reviewOptions),
      showBudget ? createTokenBudget(target) : Promise.resolve(null),
    ]);

    if (format === 'json') {
      const json = reviewResult.toJSON();
      if (budget) {
        json.budget = budget.toJSON();
      }
      return json;
    }

    let output = reviewResult.toReport();
    if (budget) {
      output += '\n\n' + budget.toReport();
    }
    return { output };
  } catch (err) {
    return { error: true, message: `Review failed: ${err.message}` };
  }
}

// ---------------------------------------------------------------------------
// Correlate — Static-Dynamic Correlation Pipeline
// ---------------------------------------------------------------------------

/**
 * Run the static-dynamic correlation pipeline on a target.
 *
 * Orchestrates: static review → normalize → build exploitation queue →
 * optional Nuclei scan → normalize dynamic findings → correlate with
 * Shannon-inspired confidence scoring → output report.
 *
 * Degrades gracefully when nuclei is not installed (static-only mode).
 *
 * @param {object|string[]} args
 * @returns {Promise<object>}
 */
export async function handleCorrelate(args = {}) {
  const argList = Array.isArray(args) ? args : [args.target].filter(Boolean);
  const target = argList.find((a) => !a.startsWith('-'));

  if (!target) {
    return {
      error: true,
      message: [
        'Usage: nerf correlate <target> [options]',
        '',
        'Options:',
        '  --format <text|json|sarif>  Output format (default: text)',
        '  --static-only               Skip dynamic scan, static analysis only',
        '  --include-noise             Include noise-classified findings',
        '  --max-queue <n>             Max exploitation queue size (default: 50)',
        '',
        'Runs a static-dynamic correlation pipeline:',
        '  1. Static code review (two-stage pipeline)',
        '  2. Normalize findings into common model',
        '  3. Build exploitation queue (CWE-based)',
        '  4. Run Nuclei scan (if available, skip gracefully if not)',
        '  5. Correlate static + dynamic findings',
        '  6. Score with Shannon-inspired confidence model',
        '  7. Output report with confidence classifications',
      ].join('\n'),
    };
  }

  const format = argList.find((a, i) => argList[i - 1] === '--format') || 'text';
  const staticOnly = argList.includes('--static-only');
  const includeNoise = argList.includes('--include-noise');
  const maxQueueArg = argList.find((a, i) => argList[i - 1] === '--max-queue');
  const maxQueueSize = maxQueueArg ? parseInt(maxQueueArg, 10) : undefined;

  try {
    const { PipelineHooks, hookableStage } = await import('../pipeline/hooks.js');
    const { FindingNormalizer, ExploitationQueueBuilder, CorrelationEngine } = await import('../correlation/index.js');
    const { twoStageReview } = await import('../review/two-stage.js');

    const hooks = new PipelineHooks();
    const stageTimes = {};

    // Register timing hooks for observability
    const trackStage = (name) => {
      hooks.hook(`before:${name}`, (ctx) => { ctx.meta._stageStart = Date.now(); }, { name: `${name}-timer-start` });
      hooks.hook(`after:${name}`, (ctx) => {
        stageTimes[name] = Date.now() - (ctx.meta._stageStart || ctx.startTime);
      }, { name: `${name}-timer-end` });
    };
    trackStage('normalize');
    trackStage('queue-build');
    trackStage('scan');
    trackStage('correlate');

    const t0 = Date.now();

    // Stage 1: Static review
    process.stderr.write('  ● Running static code review...\n');
    const reviewResult = await twoStageReview(target);
    const staticReviewFindings = [
      ...(reviewResult.stage1?.result?.findings ?? []),
      ...(reviewResult.stage2?.result?.findings ?? []),
    ];
    process.stderr.write(`  ✔ Static review: ${staticReviewFindings.length} finding(s)\n`);

    // Stage 2: Normalize static findings
    const normalizer = new FindingNormalizer();
    const normalizeFn = async () => {
      return normalizer.normalizeAll(staticReviewFindings, []);
    };
    const hookedNormalize = hookableStage(hooks, 'normalize', normalizeFn);
    const normalizedStatic = await hookedNormalize(target);
    process.stderr.write(`  ✔ Normalized: ${normalizedStatic.length} static finding(s)\n`);

    // Stage 3: Build exploitation queue
    const queueOpts = {};
    if (maxQueueSize) queueOpts.maxQueueSize = maxQueueSize;
    const queueBuilder = new ExploitationQueueBuilder(queueOpts);
    const buildQueueFn = async () => {
      return queueBuilder.buildQueue(normalizedStatic, { maxQueueSize });
    };
    const hookedQueueBuild = hookableStage(hooks, 'queue-build', buildQueueFn);
    const { queue, skipped } = await hookedQueueBuild(target);
    process.stderr.write(`  ✔ Exploitation queue: ${queue.length} dynamic target(s), ${skipped.length} static-only\n`);

    // Stage 4: Dynamic scan (optional, degrades gracefully)
    let dynamicFindings = [];
    if (!staticOnly && queue.length > 0) {
      const scanFn = async () => {
        try {
          const { NucleiRunner } = await import('../pipeline/scanner.js');
          const runner = new NucleiRunner();
          if (!runner.available) {
            process.stderr.write('  ⚠ nuclei not installed, running in static-only mode\n');
            return [];
          }
          process.stderr.write(`  ● Running Nuclei scan on ${queue.length} target(s)...\n`);
          const scanResult = await runner.scan(target, {
            profile: queue[0]?.profile,
            timeout: 120,
          });
          process.stderr.write(`  ✔ Dynamic scan: ${(scanResult.findings || []).length} finding(s)\n`);
          return scanResult.findings || [];
        } catch (scanErr) {
          process.stderr.write(`  ⚠ Dynamic scan failed: ${scanErr.message}, continuing with static-only\n`);
          return [];
        }
      };
      const hookedScan = hookableStage(hooks, 'scan', scanFn);
      const scanFindings = await hookedScan(target);
      // Normalize dynamic findings
      dynamicFindings = normalizer.normalizeAll([], scanFindings);
    } else if (staticOnly) {
      process.stderr.write('  ○ Skipping dynamic scan (--static-only)\n');
    } else {
      process.stderr.write('  ○ No dynamic targets in queue, skipping scan\n');
    }

    // Stage 5: Correlate and score
    const engine = new CorrelationEngine();
    const correlateFn = async () => {
      return engine.correlate(normalizedStatic, dynamicFindings, { includeNoise });
    };
    const hookedCorrelate = hookableStage(hooks, 'correlate', correlateFn);
    const correlated = await hookedCorrelate(target);

    const totalTime = Date.now() - t0;

    // Classify results
    const confirmed = correlated.filter(f => engine.classifyConfidence(f.confidence) === 'confirmed');
    const probable = correlated.filter(f => engine.classifyConfidence(f.confidence) === 'probable');
    const possible = correlated.filter(f => engine.classifyConfidence(f.confidence) === 'possible');

    process.stderr.write(`  ✔ Correlation complete: ${correlated.length} finding(s) ` +
      `(${confirmed.length} confirmed, ${probable.length} probable, ${possible.length} possible)\n`);

    // Format output
    if (format === 'sarif') {
      const sarifReport = engine.toSarifReport(correlated);
      return { output: JSON.stringify(sarifReport.toDict(), null, 2) };
    }

    if (format === 'json') {
      return {
        output: JSON.stringify({
          target,
          totalFindings: correlated.length,
          classifications: {
            confirmed: confirmed.length,
            probable: probable.length,
            possible: possible.length,
          },
          staticFindings: normalizedStatic.length,
          dynamicFindings: dynamicFindings.length,
          queueSize: queue.length,
          skippedCount: skipped.length,
          stageTimes,
          totalTime,
          findings: correlated.map(f => ({
            id: f.id,
            title: f.title,
            severity: f.severity,
            confidence: f.confidence,
            classification: engine.classifyConfidence(f.confidence),
            channel: f.channel,
            cweIds: f.cweIds,
            staticLocation: f.staticLocation,
            dynamicLocation: f.dynamicLocation,
          })),
        }, null, 2),
      };
    }

    // Text format (default)
    const lines = [
      '═══════════════════════════════════════════════════════',
      '  NERF Static-Dynamic Correlation Report',
      '═══════════════════════════════════════════════════════',
      '',
      `Target: ${target}`,
      `Total findings: ${correlated.length}`,
      `  Confirmed: ${confirmed.length}`,
      `  Probable:  ${probable.length}`,
      `  Possible:  ${possible.length}`,
      '',
      `Pipeline:`,
      `  Static review findings: ${staticReviewFindings.length}`,
      `  Normalized static:      ${normalizedStatic.length}`,
      `  Dynamic queue targets:  ${queue.length}`,
      `  Dynamic findings:       ${dynamicFindings.length}`,
      `  Skipped (static-only):  ${skipped.length}`,
      '',
      `Stage times:`,
      ...Object.entries(stageTimes).map(([stage, ms]) => `  ${stage}: ${ms}ms`),
      `  Total: ${totalTime}ms`,
      '',
    ];

    if (correlated.length > 0) {
      lines.push('── Findings ──');
      lines.push('');
      for (const f of correlated) {
        const classification = engine.classifyConfidence(f.confidence);
        lines.push(f.toReport());
        lines.push(`Class      : ${classification.toUpperCase()}`);
        lines.push('');
      }
    }

    return { output: lines.join('\n') };
  } catch (err) {
    return { error: true, message: `Correlation failed: ${err.message}` };
  }
}

// ---------------------------------------------------------------------------
// Panel — Expert Panel Security Assessment
// ---------------------------------------------------------------------------

/**
 * nerf panel <target> [--format json|text]
 *
 * Run 3-persona expert panel assessment: Red Team, Blue Team, Architect.
 *
 * @param {string[]|object} args
 * @returns {Promise<object>}
 */
export async function handlePanel(args = {}) {
  const argList = Array.isArray(args) ? args : [args.target].filter(Boolean);
  const target = argList.find((a) => !a.startsWith('-'));

  if (!target) {
    return {
      error: true,
      message: [
        'Usage: nerf panel <file|dir|code> [options]',
        '',
        'Options:',
        '  --format <text|json>      Output format (default: text)',
        '',
        'Runs 3 simulated expert persona reviews:',
        '  • Red Team Expert    — Attack surface, exploitation chains',
        '  • Blue Team Expert   — Detection gaps, logging, monitoring',
        '  • Architect Expert   — Trust boundaries, auth design, OWASP',
      ].join('\n'),
    };
  }

  const format = argList.find((a, i) => argList[i - 1] === '--format') || 'text';

  try {
    const { panelReview } = await import('../review/panel.js');
    const result = await panelReview(target, { format });

    if (format === 'json') {
      return result.toJSON();
    }
    return { output: result.toReport() };
  } catch (err) {
    return { error: true, message: `Panel review failed: ${err.message}` };
  }
}

// ---------------------------------------------------------------------------
// Analyze — Cross-Artifact Consistency Analyzer
// ---------------------------------------------------------------------------

/**
 * nerf analyze [--format json|text] [--root <path>]
 *
 * Scan NERF artifacts for stale references, orphans, mode mismatches,
 * coverage gaps, and structural issues.
 *
 * @param {string[]|object} args
 * @returns {Promise<object>}
 */
export async function handleAnalyze(args = {}) {
  const argList = Array.isArray(args) ? args : [];
  const format = argList.find((a, i) => argList[i - 1] === '--format') || 'text';
  const root = argList.find((a, i) => argList[i - 1] === '--root');

  try {
    const { ConsistencyAnalyzer } = await import('../analyze/consistency.js');
    const analyzer = new ConsistencyAnalyzer(root || undefined);
    const result = analyzer.analyze();

    if (format === 'json') {
      return result.toJSON();
    }
    return { output: result.toReport() };
  } catch (err) {
    return { error: true, message: `Analyze failed: ${err.message}` };
  }
}

// ---------------------------------------------------------------------------
// Shard — Semantic Document Sharding
// ---------------------------------------------------------------------------

/**
 * nerf shard <file> [--level N] [--output <dir>] [--dry-run]
 *
 * Split a large markdown document into semantically coherent chunks.
 *
 * @param {string[]|object} args
 * @returns {Promise<object>}
 */
export async function handleShard(args = {}) {
  const argList = Array.isArray(args) ? args : [args.target].filter(Boolean);
  const target = argList.find((a) => !a.startsWith('-'));

  if (!target) {
    return {
      error: true,
      message: [
        'Usage: nerf shard <file.md> [options]',
        '',
        'Options:',
        '  --level <N>        Heading level to split at (default: 2)',
        '  --output <dir>     Output directory',
        '  --dry-run          Preview shards without writing files',
        '  --format <json>    Output as JSON',
      ].join('\n'),
    };
  }

  const level = parseInt(argList.find((a, i) => argList[i - 1] === '--level') || '2', 10);
  const output = argList.find((a, i) => argList[i - 1] === '--output');
  const dryRun = argList.includes('--dry-run');
  const format = argList.find((a, i) => argList[i - 1] === '--format') || 'text';

  try {
    const { shardDocument } = await import('../analyze/sharding.js');
    const result = shardDocument(target, { level, output, dryRun });

    if (format === 'json') {
      return result.toJSON();
    }
    return { output: result.toReport() };
  } catch (err) {
    return { error: true, message: `Shard failed: ${err.message}` };
  }
}

// ---------------------------------------------------------------------------
// Guardrail — Test Input/Output Guardrails
// ---------------------------------------------------------------------------

/**
 * nerf guardrail <text> [--type input|output|both]
 *
 * Test text against NERF's guardrail tripwire system.
 *
 * @param {string[]|object} args
 * @returns {Promise<object>}
 */
export async function handleGuardrail(args = {}) {
  const argList = Array.isArray(args) ? args : [args.text].filter(Boolean);
  const text = argList.filter((a) => !a.startsWith('-')).join(' ');

  if (!text) {
    return {
      error: true,
      message: [
        'Usage: nerf guardrail <text> [options]',
        '',
        'Options:',
        '  --type <input|output|both>  Which guardrails to run (default: both)',
        '  --format <json>             Output as JSON',
        '',
        'Tests text against guardrail tripwires:',
        '  • Prompt injection detection',
        '  • Scope compliance validation',
        '  • Dangerous command detection',
        '  • Data leak prevention',
      ].join('\n'),
    };
  }

  const type = argList.find((a, i) => argList[i - 1] === '--type') || 'both';
  const format = argList.find((a, i) => argList[i - 1] === '--format') || 'text';

  try {
    const { createGuardrailEngine } = await import('../guardrails/engine.js');
    const engine = createGuardrailEngine();
    const results = await engine.audit(text);

    if (format === 'json') {
      return {
        tripped: results.length > 0,
        tripwires: results.map((r) => ({
          guardrail: r.guardrail,
          type: r.type,
          severity: r.severity,
          reason: r.reason,
          action: r.action,
        })),
      };
    }

    if (results.length === 0) {
      return { output: '✓ No guardrails tripped.' };
    }

    const lines = [`✗ ${results.length} guardrail${results.length !== 1 ? 's' : ''} tripped:`, ''];
    for (const r of results) {
      lines.push(`  [${r.severity.toUpperCase()}] ${r.guardrail} (${r.type})`);
      lines.push(`    Reason: ${r.reason}`);
      lines.push(`    Action: ${r.action}`);
    }
    return { output: lines.join('\n') };
  } catch (err) {
    return { error: true, message: `Guardrail check failed: ${err.message}` };
  }
}

// ---------------------------------------------------------------------------
// Chain — multi-mode agent chain
// ---------------------------------------------------------------------------

export async function handleChain(args = {}) {
  try {
    const { runChain } = await import('../execution-engine/handoff.js');
    const argList = Array.isArray(args) ? args : (args._ || []);

    // Parse: nerf chain "red,blue" "task description" [--verbose]
    const verbose = argList.includes('--verbose');
    const positional = argList.filter(a => !a.startsWith('--'));

    if (positional.length < 2) {
      return {
        error: true,
        message: 'Usage: nerf chain "<mode1,mode2,...>" "<task>" [--verbose]',
      };
    }

    const modes = positional[0].split(',').map(m => m.trim()).filter(Boolean);
    const task = positional.slice(1).join(' ');

    if (modes.length === 0) {
      return { error: true, message: 'No modes specified. Provide comma-separated modes.' };
    }

    const result = await runChain(modes, { task, verbose }, {});
    return { output: JSON.stringify(result, null, 2) };
  } catch (err) {
    return { error: true, message: `Chain execution failed: ${err.message}` };
  }
}

// ---------------------------------------------------------------------------
// Council — multi-model consensus (not yet implemented)
// ---------------------------------------------------------------------------

export async function handleCouncil(_args = {}) {
  // LLMCouncil / cli/lib/execution/council.js has not been implemented yet.
  // Return a clear not-implemented response rather than attempting a dynamic
  // import that would always throw ERR_MODULE_NOT_FOUND.
  return {
    error: true,
    message: 'nerf council is not yet available. Multi-model consensus is planned for a future release.',
  };
}

// ---------------------------------------------------------------------------
// Resume — resume interrupted sessions
// ---------------------------------------------------------------------------

export async function handleResume(args = {}) {
  try {
    const { listSessions, loadSession } = await import('../session/logger.js');
    const argList = Array.isArray(args) ? args : (args._ || []);

    // nerf resume --list
    if (argList.includes('--list') || argList.length === 0) {
      const sessions = listSessions({ limit: 20 });
      if (sessions.length === 0) {
        return { output: 'No sessions found. Run `nerf <mode> --auto "task"` to create one.' };
      }

      const lines = ['Recent Sessions:', ''];
      for (const s of sessions) {
        const cost = s.totalCostUSD ? ` $${s.totalCostUSD.toFixed(4)}` : '';
        const tokens = s.totalTokensIn + s.totalTokensOut;
        lines.push(`  ${s.sessionId}  ${(s.mode || '?').padEnd(12)} ${(s.status || '?').padEnd(12)} ${tokens} tokens${cost}`);
        if (s.task) lines.push(`    Task: ${s.task.slice(0, 80)}`);
      }
      return { output: lines.join('\n') };
    }

    // nerf resume <session-id>
    const sessionId = argList.find(a => !a.startsWith('--'));
    if (!sessionId) {
      return { error: true, message: 'Usage: nerf resume <session-id> | nerf resume --list' };
    }

    const session = loadSession(sessionId);
    if (!session) {
      return { error: true, message: `Session not found: ${sessionId}` };
    }

    const meta = session.metadata;
    const autoFlag = argList.includes('--auto');

    // Build resume summary
    const lines = [
      `Session: ${meta.sessionId}`,
      `Mode: ${meta.mode}`,
      `Task: ${meta.task}`,
      `Status: ${meta.status}`,
      `Interactions: ${meta.interactionCount}`,
      `Tokens: ${meta.totalTokensIn} in / ${meta.totalTokensOut} out`,
      `Cost: $${(meta.totalCostUSD || 0).toFixed(4)}`,
      '',
    ];

    // Show last few interactions
    const interactions = session.entries.filter(e => e.type === 'interaction');
    if (interactions.length > 0) {
      lines.push('Last interactions:');
      const recent = interactions.slice(-3);
      for (const e of recent) {
        lines.push(`  Turn ${e.turn}: ${e.toolCalls} tool calls, ${e.tokensIn}+${e.tokensOut} tokens`);
        if (e.outputSummary) lines.push(`    ${e.outputSummary.slice(0, 100)}`);
      }
      lines.push('');
    }

    if (meta.status === 'completed') {
      lines.push('Session already completed. Nothing to resume.');
      return { output: lines.join('\n') };
    }

    if (autoFlag) {
      // Auto-continue execution engine was removed
      lines.push('Auto-resume not available (execution engine removed).');
      lines.push('Session context shown above for manual continuation.');
    } else {
      lines.push('To auto-continue this session, run:');
      lines.push(`  nerf resume ${sessionId} --auto`);
    }

    return { output: lines.join('\n') };
  } catch (err) {
    return { error: true, message: `Resume failed: ${err.message}` };
  }
}

// ---------------------------------------------------------------------------
// Benchmark — run benchmark suites (NYU CTF, PicoCTF, OverTheWire)
// ---------------------------------------------------------------------------

export async function handleBenchmark(args = {}) {
  try {
    const argList = Array.isArray(args) ? args : (args._ || []);
    const subCommand = argList[0] || 'status';
    const suite = argList.find((_, i) => argList[i - 1] === '--suite') || 'nyu';

    // nerf benchmark status — show available benchmarks and baselines
    if (subCommand === 'status' || subCommand === 'list') {
      const { BenchmarkBuilder, HarnessConfig, ALL_BASELINES } = await import('../benchmark/index.js');
      const { enumerateNyuChallenges, NYU_BASELINES, NYU_CTF_CLONE_DIR, NYU_CATEGORIES } = await import('../benchmark/nyu-ctf.js');

      const lines = [];

      // NYU CTF
      const nyuChallenges = enumerateNyuChallenges(NYU_CTF_CLONE_DIR);
      if (nyuChallenges.length > 0) {
        const byCat = {};
        for (const c of nyuChallenges) byCat[c.category] = (byCat[c.category] || 0) + 1;
        lines.push('');
        lines.push(`NYU CTF: ${nyuChallenges.length} challenges`);
        lines.push(`  Categories: ${Object.entries(byCat).map(([c,n]) => `${c}(${n})`).join(', ')}`);
        lines.push('  Baselines:');
        for (const bl of NYU_BASELINES) lines.push(`    ${bl.name}: ${bl.overallPct.toFixed(1)}%`);
      } else {
        lines.push('');
        lines.push('NYU CTF: not cloned (nerf benchmark clone --suite nyu)');
      }

      // PicoCTF
      const { enumeratePicoChallenges, getPicoCatalogStats } = await import('../benchmark/picoctf.js');
      const picoStats = getPicoCatalogStats();
      lines.push('');
      lines.push(`PicoCTF: ${picoStats.total} challenges (bundled catalog)`);
      lines.push(`  Categories: ${Object.entries(picoStats.byCategory).map(([c,n]) => `${c}(${n})`).join(', ')}`);
      lines.push(`  Target types: ${Object.entries(picoStats.byTargetType).map(([t,n]) => `${t}(${n})`).join(', ')}`);

      // OverTheWire
      const { getOtwCatalogStats, getProgressStats } = await import('../benchmark/overthewire.js');
      const otwStats = getOtwCatalogStats();
      const otwProgress = getProgressStats();
      lines.push('');
      lines.push(`OverTheWire: ${otwStats.total} levels across ${Object.keys(otwStats.byWargame).length} wargames`);
      for (const [wg, stats] of Object.entries(otwProgress)) {
        lines.push(`  ${stats.name}: ${stats.solved}/${stats.total} solved (${stats.pct}%)`);
      }

      return { output: lines.join('\n') };
    }

    // nerf benchmark clone — clone benchmark repos
    if (subCommand === 'clone') {
      if (suite === 'pico') {
        const { clonePicoCTF, enumeratePicoChallenges } = await import('../benchmark/picoctf.js');
        clonePicoCTF();
        const challenges = enumeratePicoChallenges();
        return { output: `Initialized PicoCTF catalog: ${challenges.length} challenges` };
      }

      if (suite === 'otw') {
        const { cloneOtw, enumerateOtwChallenges } = await import('../benchmark/overthewire.js');
        cloneOtw();
        const challenges = enumerateOtwChallenges();
        return { output: `Initialized OverTheWire wargames: ${challenges.length} levels` };
      }

      if (suite === 'nyu') {
        const { NYU_CTF_REPO_URL, NYU_CTF_CLONE_DIR, enumerateNyuChallenges } = await import('../benchmark/nyu-ctf.js');
        const { existsSync } = await import('node:fs');
        const { spawnSync } = await import('node:child_process');
        const { mkdirSync } = await import('node:fs');
        const { join, dirname } = await import('node:path');

        if (existsSync(join(NYU_CTF_CLONE_DIR, '.git'))) {
          spawnSync('git', ['-C', NYU_CTF_CLONE_DIR, 'pull', '--ff-only'], { timeout: 120000, stdio: 'pipe' });
        } else {
          mkdirSync(dirname(NYU_CTF_CLONE_DIR), { recursive: true });
          spawnSync('git', ['clone', '--depth=1', NYU_CTF_REPO_URL, NYU_CTF_CLONE_DIR], { timeout: 300000, stdio: 'pipe' });
        }
        const challenges = enumerateNyuChallenges(NYU_CTF_CLONE_DIR);
        return { output: `Cloned NYU CTF benchmarks: ${challenges.length} challenges` };
      }

      return { error: true, message: 'Specify a suite: nerf benchmark clone --suite nyu|pico|otw' };
    }

    // nerf benchmark run — execute benchmarks
    if (subCommand === 'run') {
      const { BenchmarkBuilder, getSolver, runBenchmarks, generateJsonReport, generateMarkdownReport } = await import('../benchmark/index.js');
      const { writeFileSync, mkdirSync } = await import('node:fs');
      const { join } = await import('node:path');
      const { homedir } = await import('node:os');

      const all = argList.includes('--all');
      const solverName = argList.find((_, i) => argList[i - 1] === '--solver') || 'auto';
      const backend = argList.find((_, i) => argList[i - 1] === '--backend') || null;
      const concurrency = parseInt(argList.find((_, i) => argList[i - 1] === '--concurrency') || '1', 10);
      const retries = parseInt(argList.find((_, i) => argList[i - 1] === '--retries') || '1', 10);
      const levelFilter = argList.find((_, i) => argList[i - 1] === '--level');
      const tagFilter = argList.find((_, i) => argList[i - 1] === '--tag');
      const categoryFilter = argList.find((_, i) => argList[i - 1] === '--category');
      const specificNames = argList.filter(a => /^nyu-/.test(a));

      if (!all && specificNames.length === 0) {
        return {
          error: true,
          message: [
            'Usage: nerf benchmark run [options]',
            '',
            'Options:',
            '  --all                Run all benchmarks',
            '  --suite <name>       Benchmark suite: nyu (default), pico, otw',
            '  --solver <name>      Solver: auto (default), auto-multi, claude-code, remote, stub',
            '  --backend <name>     LLM backend: ollama, claude',
            '  --concurrency <n>    Parallel benchmarks (default 1)',
            '  --retries <n>        Attempts per benchmark / pass@k (default 1)',
            '  --level <n>          Filter by level (1-5)',
            '  --tag <tag>          Filter by tag',
            '  --category <cat>     Filter by category (NYU: web, pwn, rev, crypto, forensics, misc)',
            '  nyu-001 ...          Specific benchmark names',
          ].join('\n'),
        };
      }

      const builder = new BenchmarkBuilder();
      const solver = getSolver(solverName, { backend });
      const traces = [];

      process.stderr.write(`Starting ${suite} benchmark run: solver=${solverName} backend=${backend || 'auto'} concurrency=${concurrency} retries=${retries}\n`);

      // Apply category filter for NYU
      let effectiveTagFilter = tagFilter || undefined;
      if (categoryFilter && suite === 'nyu') {
        effectiveTagFilter = categoryFilter;
      }

      const report = await runBenchmarks({
        builder,
        solver,
        benchmarkNames: specificNames.length > 0 ? specificNames : undefined,
        runAll: all,
        levelFilter: levelFilter ? parseInt(levelFilter, 10) : undefined,
        tagFilter: effectiveTagFilter,
        concurrency,
        retries,
        onResult: (name, result) => {
          const status = result.passed ? '✅' : result.solverResult.error ? '⚠️' : '❌';
          process.stderr.write(`  ${status} ${name} (${result.solverResult.durationS.toFixed(1)}s)\n`);
        },
        onTrace: (event) => traces.push(event),
      });

      // Compute pass@k if retries > 1
      const passAtK = retries > 1 ? `pass@${retries}` : 'pass@1';

      // Save reports
      const reportDir = join(homedir(), '.nerf', 'benchmark-reports');
      mkdirSync(reportDir, { recursive: true });
      const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);

      const jsonPath = join(reportDir, `${suite}-${timestamp}.json`);
      writeFileSync(jsonPath, generateJsonReport(report, solverName));

      const mdPath = join(reportDir, `${suite}-${timestamp}.md`);
      writeFileSync(mdPath, generateMarkdownReport(report, solverName));

      const tracePath = join(reportDir, `${suite}-${timestamp}-traces.json`);
      writeFileSync(tracePath, JSON.stringify(traces, null, 2));

      // Category breakdown for NYU CTF
      const categoryLines = [];
      if (suite === 'nyu') {
        const byCat = {};
        for (const r of report.results) {
          const cat = r.config.category || 'misc';
          if (!byCat[cat]) byCat[cat] = { total: 0, passed: 0 };
          byCat[cat].total++;
          if (r.passed) byCat[cat].passed++;
        }
        categoryLines.push('', 'By Category:');
        for (const [cat, data] of Object.entries(byCat).sort(([a],[b]) => a.localeCompare(b))) {
          const pct = data.total > 0 ? (data.passed / data.total * 100).toFixed(1) : '0.0';
          categoryLines.push(`  ${cat}: ${data.passed}/${data.total} (${pct}%)`);
        }
      }

      // Summary output
      const lines = [
        `${suite.toUpperCase()} Benchmark Run Complete (${passAtK})`,
        `  Total: ${report.total}`,
        `  Passed: ${report.passed} (${report.passRate.toFixed(1)}%)`,
        `  Failed: ${report.failed}`,
        `  Skipped: ${report.skipped}`,
        `  Duration: ${report.durationS.toFixed(1)}s`,
        `  Cost: $${report.totalCostUsd.toFixed(4)}`,
        ...categoryLines,
        '',
        `Reports saved to:`,
        `  JSON: ${jsonPath}`,
        `  Markdown: ${mdPath}`,
        `  Traces: ${tracePath}`,
      ];

      return { output: lines.join('\n') };
    }

    return {
      error: true,
      message: 'Usage: nerf benchmark <status|clone|run> [options]',
    };
  } catch (err) {
    return { error: true, message: `Benchmark failed: ${err.message}` };
  }
}
