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

import { BaseHandler } from './base.js';

/**
 * PURPLE mode handler — detection engineering.
 * Adds: emulate, coverage, gaps
 */
export class PurpleHandler extends BaseHandler {
  constructor() { super('purple'); }

  domainActions() {
    return ['emulate', 'coverage', 'gaps'];
  }

  describeDomain(action) {
    return {
      'emulate':  'Build emulation plan pairing attack steps with detection validation',
      'coverage': 'Map ATT&CK technique coverage from detection rules',
      'gaps':     'Identify detection gaps with remediation recommendations',
    }[action];
  }

  async executeDomain(command, ctx) {
    switch (command) {
      case 'emulate':  return this.emulate(ctx);
      case 'coverage': return this.coverage(ctx);
      case 'gaps':     return this.gaps(ctx);
      default: throw new Error(`Unknown PURPLE command: ${command}`);
    }
  }

  /** Build emulation plan — attack + detection pairs. */
  emulate(ctx) {
    const { skill, technique } = ctx;
    if (!skill) throw new Error('No SKILL.md found');

    // Extract attack steps from workflow/procedure sections
    const attackSteps = [];
    const detectionSteps = [];

    for (const section of skill.sections) {
      if (/step|workflow|procedure|emulat|attack|execution/i.test(section.title)) {
        attackSteps.push({
          phase: section.title,
          content: section.content.slice(0, 400),
        });
      }
      if (/detect|valid|verify|confirm|monitor|alert/i.test(section.title)) {
        detectionSteps.push({
          phase: section.title,
          content: section.content.slice(0, 400),
        });
      }
    }

    // Pair attack commands with detection queries
    const pairs = [];
    const attackBlocks = skill.codeBlocks.filter(b =>
      ['bash', 'powershell', 'sh', 'cmd', 'python'].includes(b.lang)
    );
    const detectBlocks = skill.codeBlocks.filter(b =>
      ['yaml', 'sigma', 'kql', 'spl', 'kusto', 'splunk'].includes(b.lang)
    );

    const maxPairs = Math.min(attackBlocks.length, 10);
    for (let i = 0; i < maxPairs; i++) {
      pairs.push({
        attack: {
          lang: attackBlocks[i].lang,
          context: attackBlocks[i].context,
          command: attackBlocks[i].code.split('\n').slice(0, 5).join('\n'),
        },
        detection: detectBlocks[i] ? {
          lang: detectBlocks[i].lang,
          context: detectBlocks[i].context,
          rule: detectBlocks[i].code.split('\n').slice(0, 10).join('\n'),
        } : { note: 'No matching detection rule found — GAP' },
      });
    }

    return {
      action: 'emulate',
      technique,
      mode: 'PURPLE',
      attackIds: skill.attackIds,
      tools: skill.tools,
      attackSteps: attackSteps.slice(0, 10),
      detectionSteps: detectionSteps.slice(0, 10),
      emulationPairs: pairs,
      status: 'emulate_complete',
    };
  }

  /** Map detection coverage. */
  coverage(ctx) {
    const { skill, technique } = ctx;
    if (!skill) throw new Error('No SKILL.md found');

    const covered = [];
    const uncovered = [];

    for (const id of skill.attackIds) {
      // Check if any detection content references this ID
      const hasDetection = skill.codeBlocks.some(b =>
        (b.lang === 'yaml' || b.lang === 'sigma' || b.lang === 'kql' || b.lang === 'spl') &&
        b.code.includes(id)
      );
      if (hasDetection) {
        covered.push(id);
      } else {
        uncovered.push(id);
      }
    }

    // Count detection artifacts
    const sigmaCount = skill.codeBlocks.filter(b =>
      (b.lang === 'yaml' || b.lang === 'sigma') &&
      (b.code.includes('logsource') || b.code.includes('detection'))
    ).length;

    return {
      action: 'coverage',
      technique,
      mode: 'PURPLE',
      attackIds: skill.attackIds,
      covered,
      uncovered,
      coveragePercent: skill.attackIds.length > 0
        ? Math.round((covered.length / skill.attackIds.length) * 100)
        : 0,
      coverageGrade: this.validateCoverage(covered, uncovered),
      sigmaRuleCount: sigmaCount,
      status: 'coverage_complete',
    };
  }

  /** Assign a coverage grade based on covered vs uncovered technique counts. */
  validateCoverage(covered, uncovered) {
    if (!Array.isArray(covered) || !Array.isArray(uncovered)) return 'N/A';
    const total = covered.length + uncovered.length;
    if (total === 0) return 'N/A';
    const pct = (covered.length / total) * 100;
    if (pct >= 90) return 'A';
    if (pct >= 75) return 'B';
    if (pct >= 50) return 'C';
    if (pct >= 25) return 'D';
    return 'F';
  }

  /** Identify detection gaps. */
  gaps(ctx) {
    const { skill, technique } = ctx;
    if (!skill) throw new Error('No SKILL.md found');

    const gaps = [];

    // Attack techniques without detection rules
    for (const id of skill.attackIds) {
      const hasRule = skill.codeBlocks.some(b =>
        ['yaml', 'sigma', 'kql', 'spl'].includes(b.lang)
      );
      if (!hasRule) {
        gaps.push({
          type: 'missing_detection',
          attackId: id,
          recommendation: `Create detection rule for ${id}`,
        });
      }
    }

    // Attack code blocks without corresponding detection
    const attackBlockCount = skill.codeBlocks.filter(b =>
      ['bash', 'powershell', 'sh', 'cmd'].includes(b.lang)
    ).length;
    const detectBlockCount = skill.codeBlocks.filter(b =>
      ['yaml', 'sigma', 'kql', 'spl'].includes(b.lang)
    ).length;

    if (attackBlockCount > detectBlockCount) {
      gaps.push({
        type: 'coverage_imbalance',
        attackCommands: attackBlockCount,
        detectionRules: detectBlockCount,
        recommendation: `${attackBlockCount - detectBlockCount} attack patterns lack detection rules`,
      });
    }

    // Check for log source gaps
    const logSources = new Set();
    for (const block of skill.codeBlocks) {
      if (block.code.includes('logsource')) {
        const catMatch = block.code.match(/category:\s*(\S+)/);
        if (catMatch) logSources.add(catMatch[1]);
      }
    }

    return {
      action: 'gaps',
      technique,
      mode: 'PURPLE',
      attackIds: skill.attackIds,
      gaps,
      logSourcesCovered: [...logSources],
      status: 'gaps_complete',
    };
  }
}
