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

/**
 * PURPLE mode validator — emulation plan + Sigma detection + cross-coherence.
 *
 * Ported from execution-engine/validators/purple.py.
 * Validates PURPLE mode agent output containing TWO distinct artifacts:
 * 1. Emulation plan (JSON) — extracted with explicit `json` language tag
 * 2. Detection rule (Sigma YAML) — extracted with explicit `yaml` language tag
 * 3. Cross-coherence — Sigma ATT&CK tags must reference the same technique ID
 *
 * Uses explicit language tags for extraction (per KNOWLEDGE.md — optional groups cause cross-contamination).
 *
 * @module execution-engine/validators/purple
 */

import { ValidationResult, ModeAgentResult } from '../framework.js';
import { SigmaValidator } from './sigma.js';

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

// ATT&CK technique ID format: T followed by 4 digits, optional .NNN sub-technique
const TECHNIQUE_ID_RE = /^T\d{4}(\.\d{3})?$/;

const REQUIRED_PLAN_FIELDS = [
  'technique_id', 'technique_name', 'emulation_steps', 'expected_artifacts',
];

// ---------------------------------------------------------------------------
// Extraction helpers
// ---------------------------------------------------------------------------

/**
 * Extract emulation plan JSON from text.
 * Tries explicitly tagged ```json fences first, then bare fences, then raw text.
 *
 * @param {string} text
 * @returns {Object|null}
 */
function extractEmulationPlan(text) {
  // Try explicitly-tagged ```json fences first
  const jsonMatches = [...text.matchAll(/```json\s*\n(.*?)```/gs)].map(m => m[1]);
  for (const match of jsonMatches) {
    try {
      const parsed = JSON.parse(match.trim());
      if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
        return parsed;
      }
    } catch { /* continue */ }
  }

  // Try bare (untagged) fences
  const bareMatches = [...text.matchAll(/```\s*\n(.*?)```/gs)].map(m => m[1]);
  for (const match of bareMatches) {
    try {
      const parsed = JSON.parse(match.trim());
      if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
        return parsed;
      }
    } catch { /* continue */ }
  }

  // Fall back to raw text
  try {
    const parsed = JSON.parse(text.trim());
    if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
      return parsed;
    }
  } catch { /* ignore */ }

  return null;
}

/**
 * Extract Sigma YAML from text.
 * Looks for explicitly tagged yaml/yml code-fenced blocks first.
 * Falls back to bare fences with YAML-like content.
 *
 * @param {string} text
 * @returns {string|null}
 */
function extractSigmaYaml(text) {
  // Try explicitly-tagged ```yaml / ```yml fences
  const matches = [...text.matchAll(/```ya?ml\s*\n(.*?)```/gs)].map(m => m[1]);
  if (matches.length > 0) {
    return matches.join('\n---\n');
  }

  // Fall back to bare fences that look like YAML
  const bareMatches = [...text.matchAll(/```\s*\n(.*?)```/gs)].map(m => m[1]);
  const yamlLike = bareMatches.filter(m => /^\s*(title|logsource|detection):/m.test(m));
  if (yamlLike.length > 0) {
    return yamlLike.join('\n---\n');
  }

  return null;
}

// ---------------------------------------------------------------------------
// Structural validation
// ---------------------------------------------------------------------------

/**
 * Validate emulation plan structure.
 *
 * @param {Object} plan
 * @returns {{ errors: string[], warnings: string[] }}
 */
function validateEmulationPlan(plan) {
  const errors = [];
  const warnings = [];

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

  // technique_id format
  const techniqueId = plan.technique_id;
  if (techniqueId !== undefined) {
    if (typeof techniqueId !== 'string' || !TECHNIQUE_ID_RE.test(techniqueId)) {
      errors.push(
        `Invalid technique_id format: '${techniqueId}' ` +
        `(must match T\\d{4}(\\.\\d{3})?)`
      );
    }
  }

  // emulation_steps must be a non-empty list
  const emulationSteps = plan.emulation_steps;
  if (emulationSteps !== undefined) {
    if (!Array.isArray(emulationSteps)) {
      errors.push(`emulation_steps must be a list (got ${typeof emulationSteps})`);
    } else if (emulationSteps.length === 0) {
      errors.push('emulation_steps must not be empty');
    }
  }

  // expected_artifacts must be a non-empty list
  const expectedArtifacts = plan.expected_artifacts;
  if (expectedArtifacts !== undefined) {
    if (!Array.isArray(expectedArtifacts)) {
      errors.push(`expected_artifacts must be a list (got ${typeof expectedArtifacts})`);
    } else if (expectedArtifacts.length === 0) {
      errors.push('expected_artifacts must not be empty');
    }
  }

  return { errors, warnings };
}

/**
 * Check that Sigma rule ATT&CK tags reference the emulation plan's technique.
 *
 * @param {Object} plan
 * @param {string} sigmaText
 * @returns {{ errors: string[], warnings: string[] }}
 */
function checkCoherence(plan, sigmaText) {
  const errors = [];
  const warnings = [];

  const techniqueId = plan.technique_id || '';
  if (!techniqueId) return { errors, warnings };

  // Normalize: T1059.001 → attack.t1059.001
  const normalizedTag = `attack.${techniqueId.toLowerCase()}`;

  if (!sigmaText.toLowerCase().includes(normalizedTag)) {
    errors.push(
      `Technique ID mismatch: emulation ${techniqueId} ` +
      `not found in Sigma tags (expected ${normalizedTag})`
    );
  }

  return { errors, warnings };
}

// ---------------------------------------------------------------------------
// PurpleValidator
// ---------------------------------------------------------------------------

/**
 * Deterministic validator for PURPLE mode output.
 *
 * Scoring:
 *   - 1.0: both halves valid AND coherent
 *   - 0.5: both halves structurally valid BUT incoherent
 *   - 0.0: structural errors in either half, or neither artifact found
 */
export class PurpleValidator {
  constructor() {
    this._sigmaValidator = new SigmaValidator();
  }

  /**
   * @param {import('../framework.js').ModeAgentResult} result
   * @returns {import('../framework.js').ValidationResult}
   */
  validate(result) {
    const text = result.outputText || '';

    const allErrors = [];
    const allWarnings = [];
    let emulationValid = false;
    let detectionValid = false;
    let coherent = false;
    let techniqueId = null;

    // Extract both artifacts
    const plan = extractEmulationPlan(text);
    const sigmaText = extractSigmaYaml(text);

    // Neither found
    if (plan === null && sigmaText === null) {
      return new ValidationResult({
        valid: false,
        errors: ['No emulation plan or detection rule found'],
        warnings: [],
        score: 0.0,
        metadata: {
          emulation_valid: false,
          detection_valid: false,
          coherent: false,
          technique_id: null,
        },
      });
    }

    // Validate emulation plan
    if (plan !== null) {
      const { errors: planErrors, warnings: planWarnings } = validateEmulationPlan(plan);
      allErrors.push(...planErrors);
      allWarnings.push(...planWarnings);
      emulationValid = planErrors.length === 0;
      techniqueId = emulationValid ? (plan.technique_id || null) : null;
    } else {
      allErrors.push('No emulation plan found in output');
    }

    // Validate Sigma detection rule (delegate to SigmaValidator)
    if (sigmaText !== null) {
      const sigmaResult = this._sigmaValidator.validate(
        new ModeAgentResult({ mode: 'PURPLE', outputText: sigmaText })
      );
      for (const err of sigmaResult.errors) {
        allErrors.push(`Sigma: ${err}`);
      }
      for (const warn of sigmaResult.warnings) {
        allWarnings.push(`Sigma: ${warn}`);
      }
      detectionValid = sigmaResult.valid;
    } else {
      allErrors.push('No detection rule found in output');
    }

    // Cross-coherence (only if both halves structurally valid)
    if (emulationValid && detectionValid && plan !== null && sigmaText !== null) {
      const { errors: cohErrors, warnings: cohWarnings } = checkCoherence(plan, sigmaText);
      allErrors.push(...cohErrors);
      allWarnings.push(...cohWarnings);
      coherent = cohErrors.length === 0;
    }

    // Scoring
    const hasStructuralErrors = !emulationValid || !detectionValid;
    let score;
    if (hasStructuralErrors) {
      score = 0.0;
    } else if (!coherent) {
      score = 0.5;
    } else {
      score = 1.0;
    }

    // valid means both halves pass structural checks
    const valid = emulationValid && detectionValid;

    return new ValidationResult({
      valid,
      errors: allErrors,
      warnings: allWarnings,
      score,
      metadata: {
        emulation_valid: emulationValid,
        detection_valid: detectionValid,
        coherent,
        technique_id: techniqueId,
      },
    });
  }
}
