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

/**
 * NERF Cross-Artifact Consistency Analyzer
 *
 * Scans the NERF artifact ecosystem (commands, agents, skills, knowledge,
 * CLAUDE.md) for stale references, orphan artifacts, conflicting instructions,
 * and coverage gaps.
 *
 * @module analyze/consistency
 */

import { readFileSync, readdirSync, existsSync, statSync } from 'node:fs';
import { join, resolve, dirname, basename } from 'node:path';
import { fileURLToPath } from 'node:url';

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

// ---------------------------------------------------------------------------
// Finding types
// ---------------------------------------------------------------------------

/** @enum {string} */
export const IssueSeverity = Object.freeze({
  ERROR: 'error',
  WARNING: 'warning',
  INFO: 'info',
});

/** @enum {string} */
export const IssueCategory = Object.freeze({
  STALE_REF: 'stale-reference',
  ORPHAN: 'orphan-artifact',
  MISSING: 'missing-artifact',
  MODE_MISMATCH: 'mode-mismatch',
  COVERAGE_GAP: 'coverage-gap',
  DUPLICATE: 'duplicate',
  STRUCTURE: 'structure-issue',
});

/**
 * A single consistency issue.
 */
export class ConsistencyIssue {
  /**
   * @param {object} opts
   * @param {string} opts.category    - Issue category
   * @param {string} opts.severity    - error|warning|info
   * @param {string} opts.file        - File where the issue was found
   * @param {number} [opts.line]      - Line number
   * @param {string} opts.message     - Human-readable description
   * @param {string} [opts.reference] - The stale/missing reference
   * @param {string} [opts.suggestion] - How to fix
   */
  constructor(opts = {}) {
    this.category = opts.category ?? IssueCategory.STRUCTURE;
    this.severity = opts.severity ?? IssueSeverity.WARNING;
    this.file = opts.file ?? '';
    this.line = opts.line ?? 0;
    this.message = opts.message ?? '';
    this.reference = opts.reference ?? '';
    this.suggestion = opts.suggestion ?? '';
  }

  toReport() {
    const loc = this.line ? `${this.file}:${this.line}` : this.file;
    const sev = this.severity.toUpperCase();
    const lines = [`[${sev}] ${this.category} — ${loc}`, `  ${this.message}`];
    if (this.reference) lines.push(`  Reference: ${this.reference}`);
    if (this.suggestion) lines.push(`  Fix: ${this.suggestion}`);
    return lines.join('\n');
  }
}

// ---------------------------------------------------------------------------
// Artifact Indexer
// ---------------------------------------------------------------------------

/**
 * Index all NERF artifacts from a repo root.
 */
export class ArtifactIndex {
  /**
   * @param {string} repoRoot - Path to NERF repo root
   */
  constructor(repoRoot) {
    this.root = repoRoot;
    /** @type {Map<string, string>} command name → file path */
    this.commands = new Map();
    /** @type {Map<string, string>} agent name → file path */
    this.agents = new Map();
    /** @type {Set<string>} knowledge filenames (basename) */
    this.knowledgeFiles = new Set();
    /** @type {Map<string, {hasSkillMd: boolean, hasAgentJs: boolean, skillCount: number}>} */
    this.skills = new Map();
    /** @type {Set<string>} modes defined in CLAUDE.md */
    this.modes = new Set();
  }

  /**
   * Scan all artifact directories and populate the index.
   */
  index() {
    this._indexModes();
    this._indexCommands();
    this._indexAgents();
    this._indexKnowledge();
    this._indexSkills();
  }

  _indexModes() {
    const claudePath = join(this.root, 'CLAUDE.md');
    if (!existsSync(claudePath)) return;
    const content = readFileSync(claudePath, 'utf-8');
    // Extract modes from ### `[MODE: X]` headers
    const modeRe = /\[MODE:\s*([A-Z]+)\]/g;
    let match;
    while ((match = modeRe.exec(content)) !== null) {
      this.modes.add(match[1]);
    }
  }

  _indexCommands() {
    // Command templates live in the extension prompts dir
    const primary = join(this.root, 'src', 'resources', 'extensions', 'nerf', 'prompts');
    const fallback = join(this.root, 'commands');
    const dir = existsSync(primary) ? primary : fallback;
    if (!existsSync(dir)) return;
    for (const file of readdirSync(dir)) {
      if (!file.endsWith('.md')) continue;
      const name = file.replace(/\.md$/, '');
      this.commands.set(name, join(dir, file));
    }
  }

  _indexAgents() {
    const dir = existsSync(join(this.root, 'src', 'resources', 'agents'))
      ? join(this.root, 'src', 'resources', 'agents')
      : join(this.root, 'agents');
    if (!existsSync(dir)) return;
    for (const file of readdirSync(dir)) {
      if (!file.endsWith('.md')) continue;
      const name = file.replace(/\.md$/, '');
      this.agents.set(name, join(dir, file));
    }
  }

  _indexKnowledge() {
    const dir = join(this.root, 'knowledge');
    if (!existsSync(dir)) return;
    for (const file of readdirSync(dir)) {
      if (!file.endsWith('.md')) continue;
      this.knowledgeFiles.add(file);
    }
  }

  _indexSkills() {
    const dir = existsSync(join(this.root, 'skills'))
      ? join(this.root, 'skills')
      : join(this.root, 'src', 'resources', 'skills');
    if (!existsSync(dir)) return;
    for (const entry of readdirSync(dir, { withFileTypes: true })) {
      if (!entry.isDirectory()) continue;
      const skillDir = join(dir, entry.name);
      const hasSkillMd = existsSync(join(skillDir, 'SKILL.md'));
      const techniquesDir = join(skillDir, 'techniques');
      let hasAgentJs = false;
      let skillCount = 0;
      if (existsSync(techniquesDir)) {
        for (const sub of readdirSync(techniquesDir, { withFileTypes: true })) {
          if (sub.isDirectory()) {
            skillCount++;
            const scriptsDir = join(techniquesDir, sub.name, 'scripts');
            if (existsSync(join(scriptsDir, 'agent.js')) || existsSync(join(techniquesDir, sub.name, 'agent.js'))) {
              hasAgentJs = true;
            }
          }
        }
      }
      this.skills.set(entry.name, { hasSkillMd, hasAgentJs, skillCount });
    }
  }
}

// ---------------------------------------------------------------------------
// Consistency Checks
// ---------------------------------------------------------------------------

/**
 * Check for stale knowledge references in commands.
 * Commands reference knowledge as `knowledge/filename.md`.
 *
 * @param {ArtifactIndex} index
 * @returns {ConsistencyIssue[]}
 */
function checkCommandKnowledgeRefs(index) {
  const issues = [];
  for (const [name, filePath] of index.commands) {
    const content = readFileSync(filePath, 'utf-8');
    const lines = content.split('\n');
    for (let i = 0; i < lines.length; i++) {
      const knowledgeRe = /knowledge\/([a-zA-Z0-9_-]+\.md)/g;
      let match;
      while ((match = knowledgeRe.exec(lines[i])) !== null) {
        const ref = match[1];
        if (!index.knowledgeFiles.has(ref)) {
          issues.push(new ConsistencyIssue({
            category: IssueCategory.STALE_REF,
            severity: IssueSeverity.ERROR,
            file: `commands/${name}.md`,
            line: i + 1,
            message: `References knowledge file that does not exist.`,
            reference: `knowledge/${ref}`,
            suggestion: `Remove the reference or create knowledge/${ref}.`,
          }));
        }
      }
    }
  }
  return issues;
}

/**
 * Check for stale knowledge references in agents.
 * Agents reference knowledge as bare filenames in a YAML list.
 *
 * @param {ArtifactIndex} index
 * @returns {ConsistencyIssue[]}
 */
function checkAgentKnowledgeRefs(index) {
  const issues = [];
  for (const [name, filePath] of index.agents) {
    const content = readFileSync(filePath, 'utf-8');
    const lines = content.split('\n');
    let inKnowledge = false;
    for (let i = 0; i < lines.length; i++) {
      const line = lines[i];
      if (/^knowledge:/.test(line)) {
        inKnowledge = true;
        continue;
      }
      if (inKnowledge) {
        const itemMatch = line.match(/^\s+-\s+(.+\.md)\s*$/);
        if (itemMatch) {
          const ref = itemMatch[1].trim();
          if (!index.knowledgeFiles.has(ref)) {
            issues.push(new ConsistencyIssue({
              category: IssueCategory.STALE_REF,
              severity: IssueSeverity.ERROR,
              file: `agents/${name}.md`,
              line: i + 1,
              message: `References knowledge file that does not exist.`,
              reference: ref,
              suggestion: `Remove from knowledge list or create knowledge/${ref}.`,
            }));
          }
        } else if (!/^\s+-/.test(line) && line.trim() !== '') {
          inKnowledge = false; // End of YAML list
        }
      }
    }
  }
  return issues;
}

/**
 * Check for orphan knowledge files — not referenced by any command or agent.
 *
 * @param {ArtifactIndex} index
 * @returns {ConsistencyIssue[]}
 */
function checkOrphanKnowledge(index) {
  const issues = [];
  // Collect all referenced knowledge files
  const referenced = new Set();

  for (const [, filePath] of index.commands) {
    const content = readFileSync(filePath, 'utf-8');
    const re = /knowledge\/([a-zA-Z0-9_-]+\.md)/g;
    let match;
    while ((match = re.exec(content)) !== null) {
      referenced.add(match[1]);
    }
  }

  for (const [, filePath] of index.agents) {
    const content = readFileSync(filePath, 'utf-8');
    const lines = content.split('\n');
    let inKnowledge = false;
    for (const line of lines) {
      if (/^knowledge:/.test(line)) { inKnowledge = true; continue; }
      if (inKnowledge) {
        const itemMatch = line.match(/^\s+-\s+(.+\.md)\s*$/);
        if (itemMatch) referenced.add(itemMatch[1].trim());
        else if (!/^\s+-/.test(line) && line.trim() !== '') inKnowledge = false;
      }
    }
  }

  // Also check if knowledge files reference each other
  for (const kf of index.knowledgeFiles) {
    if (kf === '00-MASTER-INDEX.md') continue; // Index file is expected to be standalone
    if (!referenced.has(kf)) {
      issues.push(new ConsistencyIssue({
        category: IssueCategory.ORPHAN,
        severity: IssueSeverity.INFO,
        file: `knowledge/${kf}`,
        message: `Knowledge file not referenced by any command or agent.`,
        suggestion: `Add reference in a relevant command or agent, or remove if obsolete.`,
      }));
    }
  }
  return issues;
}

/**
 * Check for mode mismatches — agents/commands claiming modes not in CLAUDE.md.
 *
 * @param {ArtifactIndex} index
 * @returns {ConsistencyIssue[]}
 */
function checkModeMismatches(index) {
  const issues = [];
  if (index.modes.size === 0) return issues;

  // Check agents
  for (const [name, filePath] of index.agents) {
    const content = readFileSync(filePath, 'utf-8');
    const modeMatch = content.match(/^mode:\s*([A-Z]+)\s*$/m);
    if (modeMatch) {
      const mode = modeMatch[1];
      if (!index.modes.has(mode)) {
        issues.push(new ConsistencyIssue({
          category: IssueCategory.MODE_MISMATCH,
          severity: IssueSeverity.WARNING,
          file: `agents/${name}.md`,
          message: `Agent declares mode "${mode}" which is not defined in CLAUDE.md.`,
          suggestion: `Add [MODE: ${mode}] to CLAUDE.md or fix the agent mode.`,
        }));
      }
    }
  }

  // Check commands for MODE references
  for (const [name, filePath] of index.commands) {
    const content = readFileSync(filePath, 'utf-8');
    const modeRe = /\[MODE:\s*([A-Z]+)\]/g;
    let match;
    while ((match = modeRe.exec(content)) !== null) {
      if (!index.modes.has(match[1])) {
        issues.push(new ConsistencyIssue({
          category: IssueCategory.MODE_MISMATCH,
          severity: IssueSeverity.WARNING,
          file: `commands/${name}.md`,
          message: `Command references mode "${match[1]}" not defined in CLAUDE.md.`,
          suggestion: `Fix the mode reference or add the mode to CLAUDE.md.`,
        }));
      }
    }
  }
  return issues;
}

/**
 * Check for structural issues in skills.
 *
 * @param {ArtifactIndex} index
 * @returns {ConsistencyIssue[]}
 */
function checkSkillStructure(index) {
  const issues = [];
  for (const [name, info] of index.skills) {
    if (!info.hasSkillMd) {
      issues.push(new ConsistencyIssue({
        category: IssueCategory.MISSING,
        severity: IssueSeverity.WARNING,
        file: `skills/${name}/`,
        message: `Skill directory missing SKILL.md.`,
        suggestion: `Add SKILL.md with name, description, domain, tags.`,
      }));
    }
    if (info.skillCount > 0 && !info.hasAgentJs) {
      issues.push(new ConsistencyIssue({
        category: IssueCategory.MISSING,
        severity: IssueSeverity.INFO,
        file: `skills/${name}/`,
        message: `Skill has ${info.skillCount} techniques but no agent.js found in any.`,
        suggestion: `Add agent.js to technique subdirectories for auto execution.`,
      }));
    }
  }
  return issues;
}

/**
 * Check for duplicate skill names (same directory name pattern).
 *
 * @param {ArtifactIndex} index
 * @returns {ConsistencyIssue[]}
 */
function checkDuplicateSkills(index) {
  // Look for nerf-X skills that overlap with X skills
  const issues = [];
  const names = [...index.skills.keys()];
  const nerfPrefixed = names.filter((n) => n.startsWith('nerf-'));
  for (const cp of nerfPrefixed) {
    const base = cp.replace(/^nerf-/, '');
    // Check if there's a non-prefixed version or similar
    const similar = names.filter(
      (n) => n !== cp && (n === base || n.endsWith(`-${base}`) || n.startsWith(`${base}-`)),
    );
    if (similar.length > 0) {
      issues.push(new ConsistencyIssue({
        category: IssueCategory.DUPLICATE,
        severity: IssueSeverity.INFO,
        file: `skills/${cp}/`,
        message: `Possible overlap with: ${similar.join(', ')}.`,
        suggestion: `Verify these are distinct skills, not duplicates.`,
      }));
    }
  }
  return issues;
}

// ---------------------------------------------------------------------------
// ConsistencyAnalyzer — main orchestrator
// ---------------------------------------------------------------------------

/**
 * Run all consistency checks and return unified results.
 */
export class ConsistencyAnalyzer {
  /**
   * @param {string} [repoRoot] - Path to NERF repo root (auto-detected if omitted)
   */
  constructor(repoRoot) {
    this.root = repoRoot ?? this._findRoot();
    this.index = new ArtifactIndex(this.root);
  }

  _findRoot() {
    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, 'CLAUDE.md'))) {
        return dir;
      }
      const parent = dirname(dir);
      if (parent === dir) break;
      dir = parent;
    }
    return process.cwd();
  }

  /**
   * Run all checks.
   * @returns {ConsistencyResult}
   */
  analyze() {
    const t0 = Date.now();
    this.index.index();

    const issues = [
      ...checkCommandKnowledgeRefs(this.index),
      ...checkAgentKnowledgeRefs(this.index),
      ...checkOrphanKnowledge(this.index),
      ...checkModeMismatches(this.index),
      ...checkSkillStructure(this.index),
      ...checkDuplicateSkills(this.index),
    ];

    // Sort: errors first, then warnings, then info
    const sevRank = { error: 2, warning: 1, info: 0 };
    issues.sort((a, b) => (sevRank[b.severity] ?? 0) - (sevRank[a.severity] ?? 0));

    return new ConsistencyResult({
      issues,
      stats: {
        commands: this.index.commands.size,
        agents: this.index.agents.size,
        knowledgeFiles: this.index.knowledgeFiles.size,
        skills: this.index.skills.size,
        modes: this.index.modes.size,
      },
      totalTime: Date.now() - t0,
    });
  }
}

// ---------------------------------------------------------------------------
// ConsistencyResult
// ---------------------------------------------------------------------------

export class ConsistencyResult {
  constructor({ issues = [], stats = {}, totalTime = 0 } = {}) {
    this.issues = issues;
    this.stats = stats;
    this.totalTime = totalTime;
  }

  get counts() {
    const c = { error: 0, warning: 0, info: 0 };
    for (const issue of this.issues) {
      c[issue.severity] = (c[issue.severity] ?? 0) + 1;
    }
    return c;
  }

  get summary() {
    const c = this.counts;
    const parts = [];
    if (c.error) parts.push(`${c.error} error${c.error !== 1 ? 's' : ''}`);
    if (c.warning) parts.push(`${c.warning} warning${c.warning !== 1 ? 's' : ''}`);
    if (c.info) parts.push(`${c.info} info`);
    const total = this.issues.length;
    const detail = parts.length ? ` (${parts.join(', ')})` : '';
    return `${total} issue${total !== 1 ? 's' : ''}${detail} — scanned ${this.stats.commands} commands, ${this.stats.agents} agents, ${this.stats.knowledgeFiles} knowledge docs, ${this.stats.skills} skill domains in ${this.totalTime}ms`;
  }

  toReport() {
    const lines = [
      '═══════════════════════════════════════════════════════',
      '  NERF Artifact Consistency Report',
      '═══════════════════════════════════════════════════════',
      '',
      `Summary: ${this.summary}`,
      '',
    ];

    if (this.issues.length === 0) {
      lines.push('No issues found. All artifacts are consistent.');
    } else {
      // Group by category
      const grouped = new Map();
      for (const issue of this.issues) {
        if (!grouped.has(issue.category)) grouped.set(issue.category, []);
        grouped.get(issue.category).push(issue);
      }

      for (const [category, catIssues] of grouped) {
        lines.push(`── ${category} (${catIssues.length}) ──`);
        for (const issue of catIssues) {
          lines.push(issue.toReport());
        }
        lines.push('');
      }
    }

    lines.push('───────────────────────────────────────────────────────');
    lines.push(`Artifact counts: ${this.stats.commands} commands, ${this.stats.agents} agents, ${this.stats.knowledgeFiles} knowledge, ${this.stats.skills} skill domains, ${this.stats.modes} modes`);

    return lines.join('\n');
  }

  toJSON() {
    return {
      summary: this.summary,
      counts: this.counts,
      stats: this.stats,
      totalTime: this.totalTime,
      issues: this.issues.map((i) => ({
        category: i.category,
        severity: i.severity,
        file: i.file,
        line: i.line,
        message: i.message,
        reference: i.reference,
        suggestion: i.suggestion,
      })),
    };
  }
}
