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

/**
 * Sigma rule validator for BLUE mode agent output.
 *
 * Ported from execution-engine/validators/sigma.py.
 * Validates LLM-generated Sigma detection rules for structural correctness:
 * required fields, recommended fields, value constraints, detection structure,
 * logsource structure, and ATT&CK tag format.
 *
 * Uses yaml npm package's YAML.parseAllDocuments() for multi-document YAML.
 * Uses findall+join pattern for code fence stripping (per KNOWLEDGE.md).
 *
 * @module execution-engine/validators/sigma
 */

import YAML from 'yaml';
import { ValidationResult } from '../framework.js';

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

const REQUIRED_FIELDS = ['title', 'logsource', 'detection'];

const RECOMMENDED_FIELDS = [
  'id', 'status', 'description', 'level', 'tags', 'falsepositives',
];

const VALID_LEVELS = ['critical', 'high', 'medium', 'low', 'informational'];

const VALID_STATUSES = ['stable', 'test', 'experimental', 'deprecated', 'unsupported'];

// Matches ATT&CK technique IDs like attack.t1059 or attack.t1059.001
const ATTACK_TECHNIQUE_RE = /attack\.t\d{4}(\.\d{3})?/;

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

/**
 * Extract YAML content from markdown code fences, or return text as-is.
 * Uses findall+join (not replace) to discard surrounding prose.
 *
 * @param {string} text
 * @returns {string}
 */
export function stripCodeFences(text) {
  const matches = [...text.matchAll(/```(?:ya?ml)?\s*\n(.*?)```/gs)].map(m => m[1]);
  if (matches.length > 0) {
    return matches.join('\n---\n');
  }
  return text;
}

/**
 * Parse multi-document YAML into a list of objects.
 * Uses YAML.parseAllDocuments() then .toJSON() on each document, filtering nulls.
 *
 * @param {string} text
 * @returns {Object[]}
 * @throws {Error} If YAML parsing fails
 */
export function parseYamlDocuments(text) {
  const stripped = stripCodeFences(text);
  let docs;
  try {
    docs = YAML.parseAllDocuments(stripped);
  } catch (e) {
    throw new Error(`YAML parse error: ${e.message}`);
  }

  return docs
    .map(doc => {
      if (doc.errors && doc.errors.length > 0) {
        throw new Error(`YAML parse error: ${doc.errors[0].message}`);
      }
      return doc.toJSON();
    })
    .filter(doc => doc != null);
}

/**
 * Validate a single Sigma rule dict.
 *
 * @param {Object} rule
 * @returns {{ errors: string[], warnings: string[] }}
 */
function validateSingleRule(rule) {
  const errors = [];
  const warnings = [];

  // Required fields
  for (const fieldName of REQUIRED_FIELDS) {
    if (!(fieldName in rule)) {
      errors.push(`Missing required field: ${fieldName}`);
    }
  }

  // Recommended fields
  for (const fieldName of RECOMMENDED_FIELDS) {
    if (!(fieldName in rule)) {
      warnings.push(`Missing recommended field: ${fieldName}`);
    }
  }

  // Level validation
  if ('level' in rule && !VALID_LEVELS.includes(rule.level)) {
    errors.push(
      `Invalid level: ${rule.level} (must be one of ${JSON.stringify(VALID_LEVELS)})`
    );
  }

  // Status validation (warning, not error)
  if ('status' in rule && !VALID_STATUSES.includes(rule.status)) {
    warnings.push(`Non-standard status: ${rule.status}`);
  }

  // Detection structure
  const detection = rule.detection;
  if (detection && typeof detection === 'object' && !Array.isArray(detection)) {
    if (!('condition' in detection)) {
      errors.push("Detection missing 'condition' field");
    }
    if (Object.keys(detection).length < 2) {
      warnings.push('Detection has condition but no selection criteria');
    }
  }

  // Logsource structure
  const logsource = rule.logsource;
  if (logsource && typeof logsource === 'object' && !Array.isArray(logsource)) {
    if (!('category' in logsource) && !('product' in logsource) && !('service' in logsource)) {
      warnings.push('Logsource missing category/product/service');
    }
  }

  // ATT&CK tags
  const tags = rule.tags;
  if (Array.isArray(tags) && tags.length > 0) {
    const hasTechniqueId = tags.some(tag => ATTACK_TECHNIQUE_RE.test(String(tag)));
    if (!hasTechniqueId) {
      warnings.push(
        'Tags contain no ATT&CK technique IDs ' +
        '(expected pattern: attack.tNNNN or attack.tNNNN.NNN)'
      );
    }
  }

  return { errors, warnings };
}

// ---------------------------------------------------------------------------
// SigmaValidator
// ---------------------------------------------------------------------------

/**
 * Deterministic validator for Sigma detection rule YAML.
 *
 * Scoring:
 *   - 1.0: no errors and no warnings (clean)
 *   - 0.5: warnings only (valid but imperfect)
 *   - 0.0: any errors (invalid)
 */
export class SigmaValidator {
  /**
   * Validate Sigma rule(s) in the agent result's output text.
   *
   * @param {import('../framework.js').ModeAgentResult} result
   * @returns {import('../framework.js').ValidationResult}
   */
  validate(result) {
    const text = result.outputText || '';

    // Parse YAML
    let documents;
    try {
      documents = parseYamlDocuments(text);
    } catch (e) {
      return new ValidationResult({
        valid: false,
        errors: [e.message],
        warnings: [],
        score: 0.0,
        metadata: { rules_count: 0 },
      });
    }

    // Empty output
    if (documents.length === 0) {
      return new ValidationResult({
        valid: false,
        errors: ['No Sigma rules found in output'],
        warnings: [],
        score: 0.0,
        metadata: { rules_count: 0 },
      });
    }

    // Validate each document
    const allErrors = [];
    const allWarnings = [];
    const rules = [];

    for (let i = 0; i < documents.length; i++) {
      const doc = documents[i];
      if (typeof doc !== 'object' || doc === null || Array.isArray(doc)) {
        allErrors.push(
          `Document ${i + 1} is not a YAML mapping ` +
          `(got ${Array.isArray(doc) ? 'Array' : typeof doc})`
        );
        continue;
      }

      rules.push(doc);
      const { errors: errs, warnings: warns } = validateSingleRule(doc);

      // Prefix multi-rule errors with document index
      if (documents.length > 1) {
        allErrors.push(...errs.map(e => `Rule ${i + 1}: ${e}`));
        allWarnings.push(...warns.map(w => `Rule ${i + 1}: ${w}`));
      } else {
        allErrors.push(...errs);
        allWarnings.push(...warns);
      }
    }

    // Scoring
    let score;
    if (allErrors.length > 0) {
      score = 0.0;
    } else if (allWarnings.length > 0) {
      score = 0.5;
    } else {
      score = 1.0;
    }

    return new ValidationResult({
      valid: allErrors.length === 0,
      errors: allErrors,
      warnings: allWarnings,
      score,
      metadata: { rules_count: rules.length },
    });
  }
}
