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

/**
 * Reflexion Error Learning — corrections log for pattern-based error prevention.
 *
 * Stores error patterns and corrections in a JSONL file. Before generating
 * structured output (Sigma rules, KQL queries, etc.), checks the corrections
 * log and applies known fixes automatically.
 *
 * Inspired by SuperClaude's ReflectionEngine pattern.
 *
 * @module gates/corrections
 */

import { readFileSync, appendFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { homedir } from 'node:os';

// ---------------------------------------------------------------------------
// Correction entry
// ---------------------------------------------------------------------------

/**
 * @typedef {Object} CorrectionEntry
 * @property {string} id — Unique correction ID
 * @property {string} category — Error category (sigma, kql, spl, hardening, general)
 * @property {string} pattern — Regex pattern string that matches the error
 * @property {string} replacement — Replacement string or fix description
 * @property {string} description — Human-readable description of the error
 * @property {string} timestamp — ISO timestamp
 * @property {number} applyCount — Times this correction has been applied
 */

// ---------------------------------------------------------------------------
// CorrectionsLog
// ---------------------------------------------------------------------------

const DEFAULT_DIR = join(homedir(), '.nerf', 'data');
const MAX_ENTRIES = 500;

export class CorrectionsLog {
  /**
   * @param {string} [logPath] — Path to corrections.jsonl file
   */
  constructor(logPath) {
    this._path = logPath || join(DEFAULT_DIR, 'corrections.jsonl');
    this._entries = null; // Lazy load
  }

  /**
   * Load entries from disk.
   * @returns {CorrectionEntry[]}
   */
  _load() {
    if (this._entries !== null) return this._entries;

    this._entries = [];
    if (!existsSync(this._path)) return this._entries;

    const lines = readFileSync(this._path, 'utf8').split('\n').filter(l => l.trim());
    for (const line of lines) {
      try {
        this._entries.push(JSON.parse(line));
      } catch { /* skip malformed lines */ }
    }

    return this._entries;
  }

  /**
   * Record a new correction.
   * @param {{ category: string, pattern: string, replacement: string, description: string }} opts
   * @returns {CorrectionEntry}
   */
  record(opts) {
    const entry = {
      id: `COR-${Date.now().toString(36)}`,
      category: opts.category || 'general',
      pattern: opts.pattern,
      replacement: opts.replacement || '',
      description: opts.description || '',
      timestamp: new Date().toISOString(),
      applyCount: 0,
    };

    mkdirSync(dirname(this._path), { recursive: true });
    appendFileSync(this._path, JSON.stringify(entry) + '\n');

    // Invalidate cache
    this._entries = null;

    return entry;
  }

  /**
   * Get all corrections, optionally filtered by category.
   * @param {string} [category]
   * @returns {CorrectionEntry[]}
   */
  getAll(category) {
    const entries = this._load();
    if (category) {
      return entries.filter(e => e.category === category);
    }
    return entries;
  }

  /**
   * Apply known corrections to text.
   * @param {string} text — Input text to correct
   * @param {string} category — Category to filter corrections by
   * @returns {{ corrected: string, applied: string[], count: number }}
   */
  apply(text, category) {
    const corrections = this.getAll(category);
    let corrected = text;
    const applied = [];

    for (const entry of corrections) {
      try {
        const regex = new RegExp(entry.pattern, 'g');
        if (regex.test(corrected)) {
          corrected = corrected.replace(regex, entry.replacement);
          applied.push(entry.id);
          entry.applyCount++;
        }
      } catch {
        // Invalid regex — skip
      }
    }

    return { corrected, applied, count: applied.length };
  }

  /**
   * Get correction statistics.
   * @returns {{ total: number, byCategory: Record<string, number>, topApplied: CorrectionEntry[] }}
   */
  stats() {
    const entries = this._load();
    const byCategory = {};
    for (const e of entries) {
      byCategory[e.category] = (byCategory[e.category] || 0) + 1;
    }

    const topApplied = [...entries]
      .sort((a, b) => (b.applyCount || 0) - (a.applyCount || 0))
      .slice(0, 10);

    return { total: entries.length, byCategory, topApplied };
  }

  /**
   * Prune old entries, keeping only the most recent per category.
   * @param {number} [maxPerCategory=100]
   * @returns {number} — Number of entries pruned
   */
  prune(maxPerCategory = 100) {
    const entries = this._load();
    if (entries.length <= maxPerCategory) return 0;

    const byCategory = {};
    for (const e of entries) {
      if (!byCategory[e.category]) byCategory[e.category] = [];
      byCategory[e.category].push(e);
    }

    const kept = [];
    for (const [, catEntries] of Object.entries(byCategory)) {
      catEntries.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
      kept.push(...catEntries.slice(0, maxPerCategory));
    }

    const pruned = entries.length - kept.length;
    if (pruned > 0) {
      mkdirSync(dirname(this._path), { recursive: true });
      const content = kept.map(e => JSON.stringify(e)).join('\n') + '\n';
      // Atomic write
      writeFileSync(this._path, content);
      this._entries = null;
    }

    return pruned;
  }

  /** Number of entries */
  get size() {
    return this._load().length;
  }
}

// ---------------------------------------------------------------------------
// Pre-built Sigma corrections
// ---------------------------------------------------------------------------

/** Common Sigma rule errors that LLMs make */
export const SIGMA_CORRECTIONS = [
  {
    category: 'sigma',
    pattern: 'level:\\s*(?:critical|high|medium|low)\\s*\\n(?!tags:)',
    replacement: '',
    description: 'Sigma level must be one of: critical, high, medium, low, informational',
  },
  {
    category: 'sigma',
    pattern: 'status:\\s*(?:new|draft)\\b',
    replacement: 'status: experimental',
    description: 'Sigma status "new" or "draft" should be "experimental"',
  },
  {
    category: 'sigma',
    pattern: 'logsource:\\s*\\n\\s*service:\\s*',
    replacement: 'logsource:\n    category: ',
    description: 'Sigma logsource should use "category" not "service" as primary field',
  },
];
