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

/**
 * mode.js — Mode detection for NERF gateway.
 *
 * Ports Python gateway/mode.py with behavioral fidelity:
 * - parseKeywordTable: extract mode→keywords from CLAUDE.md trigger table
 * - detectMode: route message to operating mode via explicit prefix or keyword scoring
 * - stripModePrefix: remove [MODE: X] prefix from message text
 *
 * @module gateway/mode
 */

// Matches [MODE: X] at the start of a message, case-insensitive.
const EXPLICIT_RE = /^\[MODE:\s*(\w+)\]/i;

// Matches the trigger keywords table block inside CLAUDE.md.
// Python: re.DOTALL → JS `s` flag, re.IGNORECASE → `i` flag.
const TABLE_HEADER_RE = /Trigger Keywords.*?\n\|[-| ]+\|\n(.*?)(?:\n-+|\n\n|$)/si;

// Individual data row: | WORD | content |
// Python: re.MULTILINE → JS `m` flag.
const TABLE_ROW_RE = /^\|\s*(\w+)\s*\|\s*(.+?)\s*\|/gm;

// Known header/separator cell values to skip.
const SKIP_ROW_NAMES = new Set(['MODE', '---']);

// ---------------------------------------------------------------------------
// Supplementary keywords — ported verbatim from Python mode.py
// ---------------------------------------------------------------------------

/** @type {Record<string, string[]>} */
const SUPPLEMENTARY_KEYWORDS = {
  RED: [
    // Tools
    'smb', 'nmap', 'bloodhound', 'mimikatz', 'impacket', 'cobaltstrike',
    'cobalt strike', 'msfconsole', 'metasploit', 'hashcat', 'john the ripper',
    'burp suite', 'sqlmap', 'ettercap', 'bettercap', 'scapy', 'hydra',
    'crackmapexec', 'evil-winrm', 'chisel', 'ligolo', 'rubeus', 'certipy',
    'responder', 'beef', 'empire', 'sliver', 'havoc',
    // Techniques
    'brute force', 'credential stuffing', 'credential dumping', 'kerberoast',
    'as-rep', 'pentest', 'penetration test', 'post-exploitation', 'exfiltrate',
    'exfiltration', 'privilege escalation', 'escalate privilege', 'initial access',
    'dcsync', 'secretsdump', 'pass the hash', 'pass the ticket', 'golden ticket',
    'silver ticket', 'ntlm relay', 'sql injection', 'xss', 'cross-site scripting',
    'command injection', 'buffer overflow', 'heap spray', 'rop chain', 'shellcode',
    'arp spoofing', 'arp poisoning', 'arp cache poisoning', 'man in the middle',
    'man-in-the-middle', 'session hijacking', 'dns tunneling', 'dns poisoning',
    'phishing campaign', 'phishing website', 'phishing email', 'spear phishing',
    'spear-phishing', 'covert channel', 'webshell', 'web shell',
    'reverse engineering', 'dll injection', 'process injection',
    'token impersonation', 'tokenimpersonation', 'password spraying',
    'password spray', 'kerberos', 'ntlm', 'lsass', 'sam database',
    'crack password', 'crack hash', 'pivot', 'pivoting',
    // Attack types
    'vlan hopping', 'ssl stripping', 'ssl strip', 'syn flood', 'ddos attack',
    'ddos', 'dos attack', 'malicious usb', 'rubber ducky', 'evil twin',
    'rogue ap', 'poisoning attack', 'spoofing attack', 'injection attack',
    'race condition exploit', 'deserialization', 'ssrf', 'csrf', 'lfi', 'rfi',
    'path traversal attack', 'directory traversal', 'xml external entity', 'xxe',
    'ssti', 'template injection', 'type juggling', 'prototype pollution',
    'insecure deserialization',
  ],

  BLUE: [
    // Tools
    'splunk', 'elastic', 'elasticsearch', 'kibana', 'sentinel', 'kql', 'spl',
    'yara', 'snort', 'suricata', 'zeek', 'osquery', 'wazuh', 'velociraptor',
    'carbon black', 'crowdstrike', 'elk stack', 'logstash', 'graylog', 'qradar',
    'arcsight', 'chronicle', 'datadog security',
    // Techniques
    'alert rule', 'alert rules', 'detect malware', 'detect lateral',
    'detect exfiltration', 'detect anomal', 'log correlation', 'log monitoring',
    'baseline', 'false positive', 'false positives', 'tuning', 'rule tuning',
    'ioc detection', 'behavioral detection', 'anomaly detection',
    'network monitoring', 'file integrity', 'audit policy', 'audit policies',
    'windows event', 'event log', 'syslog', 'sysmon',
    // From scale eval failures
    'honeypot', 'honeynet', 'deception technology', 'canary token',
    'default credential', 'continuous monitoring', 'security automation',
    'security playbook', 'soar', 'dwell time', 'mean time to detect', 'mttd',
    'mean time to respond', 'mttr', 'security posture', 'threat visibility',
    'security stack', 'powershell logging',
  ],

  PURPLE: [
    'att&ck coverage', 'mitre coverage', 'detection gap', 'detection gaps',
    'coverage gap', 'coverage gaps', 'emulate', 'adversary emulation',
    'atomic red team', 'caldera', 'purple team', 'detection coverage',
    'how well do our', 'gaps in our', 'gaps in detecting',
    'test our detection', 'test our defenses', 'simulate attack',
    'simulate an attack', 'rules against', 'evaluate our',
    'effectiveness of our',
  ],

  INCIDENT: [
    'compromise', 'compromised', 'ransomware', 'malware found', 'we found',
    'active breach', 'evidence collection', 'got an alert', 'beacon callback',
    'what do we do', 'we detected', 'suspicious activity', 'active incident',
    'we suspect', 'forensic analysis', 'forensic investigation',
    'memory forensics', 'disk forensics', 'how do we proceed',
    'how should we respond', 'how do we contain', 'contain the breach',
    'containment plan', 'our siem shows', 'our ids detected',
    'our firewall logs show', 'signs of lateral', 'unusual traffic',
    'unusual activity', 'incident runbook', 'evidence preservation',
    'chain of custody', 'root cause analysis', 'post-incident', 'after-action',
  ],

  RECON: [
    'enumerate subdomains', 'subdomain enumeration', 'dns recon', 'shodan',
    'censys', 'whois', 'footprinting', 'footprint', 'gather intelligence',
    'intelligence gathering', 'attack surface', 'asset discovery',
    'exposed services', 'exposed assets', 'open ports', 'port scanning',
    'banner grabbing', 'google dorking', 'google dork', 'github recon',
    'linkedin', 'email harvesting', 'email addresses', 'amass', 'subfinder',
    'httpx', 'theharvester', 'recon-ng', 'maltego', 'spiderfoot', 
    'cloud enumeration', 'digital presence', 'network footprint',
  ],

  BUILD: [
    // Actions
    'write code', 'build tool', 'create script', 'implement', 'develop',
    'automate', 'refactor', 'debug', 'deploy', 'scaffold',
    // DevSecOps
    'ci/cd', 'cicd', 'pipeline security', 'github actions', 'gitlab ci',
    'sast tool', 'dast tool', 'sca tool', 'sbom', 'slsa', 'container scanning',
    'dockerfile', 'docker compose', 'terraform module', 'terraform', 'ansible playbook',
    'ansible', 'helm chart',
    // Tooling
    'nuclei template', 'custom scanner', 'soar playbook', 'chatops',
    'automation script', 'python script', 'bash script', 'node script',
    'security tool', 'cli tool', 'api endpoint',
    // Code quality
    'unit test', 'integration test', 'lint', 'type check', 'code review',
    'pull request', 'merge request', 'pr review',
    // Infrastructure as code
    'infrastructure as code', 'iac', 'cloudformation', 'pulumi',
  ],
};

// Keywords too generic to be decisive — score 0.5 instead of word count.
const GENERIC_KEYWORDS = new Set([
  'enumerate', 'credential', 'persistence', 'detection',
  'att&ck', 'network', 'firewall',
]);

// Defensive signals that boost BLUE and reduce RED.
const DEFENSIVE_SIGNALS = [
  'detect', 'detecting', 'alert on', 'alerting', 'monitor for',
  'monitoring', 'hunt for', 'hunting', 'identify indicator',
  'write a rule', 'write a query', 'kql ',
  'yara rule', 'snort rule', 'suricata rule',
  'indicators of', 'how do i detect', 'how can i detect',
  'how to detect', 'for detecting', 'rule for',
  'detection rule', 'to identify potential', 'to detect',
];

// Incident context signals.
const INCIDENT_SIGNALS = [
  'we suspect', 'we found', 'we detected', 'our siem',
  'our ids', 'our firewall', 'how do we', 'how should we',
  'contain', 'respond to', 'triage', 'runbook',
];

// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------

/**
 * Parse the `| Mode | Trigger Keywords |` table from CLAUDE.md text.
 *
 * Returns a dict mapping UPPERCASE mode names to arrays of lowercase,
 * stripped keyword strings. Returns empty object if the table is absent.
 *
 * @param {string} claudeMdText - Raw CLAUDE.md content
 * @returns {Record<string, string[]>}
 */
export function parseKeywordTable(claudeMdText) {
  if (!claudeMdText || !claudeMdText.trim()) {
    return {};
  }

  let rowsText;
  const headerMatch = TABLE_HEADER_RE.exec(claudeMdText);
  if (!headerMatch) {
    // Try a looser parse: scan full text for table rows
    if (!claudeMdText.includes('Trigger Keywords')) {
      return {};
    }
    rowsText = claudeMdText;
  } else {
    rowsText = headerMatch[0];
  }

  /** @type {Record<string, string[]>} */
  const result = {};

  // Reset lastIndex since TABLE_ROW_RE has /g flag
  TABLE_ROW_RE.lastIndex = 0;
  let rowMatch;
  while ((rowMatch = TABLE_ROW_RE.exec(rowsText)) !== null) {
    const mode = rowMatch[1].trim().toUpperCase();

    // Skip table header row and separators
    if (SKIP_ROW_NAMES.has(mode)) continue;

    const rawKeywords = rowMatch[2];
    // Skip if the "keyword" cell looks like a column header
    if (rawKeywords.toLowerCase().includes('trigger keywords')) continue;

    const keywords = rawKeywords
      .split(',')
      .map(kw => kw.trim().toLowerCase())
      .filter(Boolean);

    if (mode && keywords.length) {
      result[mode] = keywords;
    }
  }

  // Merge supplementary keywords
  for (const [mode, extraKws] of Object.entries(SUPPLEMENTARY_KEYWORDS)) {
    if (result[mode]) {
      const existing = new Set(result[mode]);
      for (const kw of extraKws) {
        if (!existing.has(kw)) {
          result[mode].push(kw);
        }
      }
    } else {
      result[mode] = [...extraKws];
    }
  }

  return result;
}

/**
 * Detect the NERF operating mode for a given message.
 *
 * Detection order:
 * 1. Explicit [MODE: X] prefix → returns immediately
 * 2. Weighted keyword scoring with supplementary keywords,
 *    defensive/incident boosting, and tie-breaking disambiguation
 *
 * @param {string} message - Raw user message text
 * @param {Record<string, string[]>} keywordTable - From parseKeywordTable
 * @returns {[string, boolean]} [mode, needsClarification]
 */
export function detectMode(message, keywordTable) {
  // 1. Check explicit prefix.
  const prefixMatch = message.match(EXPLICIT_RE);
  if (prefixMatch) {
    return [prefixMatch[1].toUpperCase(), false];
  }

  // 2. Weighted keyword scoring.
  const lowerMessage = message.toLowerCase();
  /** @type {Record<string, number>} */
  const modeScores = {};

  for (const [mode, keywords] of Object.entries(keywordTable)) {
    let score = 0;
    for (const keyword of keywords) {
      if (lowerMessage.includes(keyword)) {
        // Multi-word phrases are more specific → higher weight
        const wordCount = keyword.split(/\s+/).length;
        const kwScore = GENERIC_KEYWORDS.has(keyword) ? 0.5 : wordCount;
        score = Math.max(score, kwScore);
      }
    }
    if (score > 0) {
      modeScores[mode] = score;
    }
  }

  // 3. Intent-based disambiguation
  const hasDefensive = DEFENSIVE_SIGNALS.some(sig => lowerMessage.includes(sig));
  const hasIncident = INCIDENT_SIGNALS.some(sig => lowerMessage.includes(sig));

  if (hasDefensive && modeScores.BLUE !== undefined && modeScores.RED !== undefined) {
    // Defensive context: boost BLUE, reduce RED
    // But NOT if PURPLE is already scoring well
    if (modeScores.PURPLE === undefined || modeScores.PURPLE < (modeScores.BLUE || 0)) {
      modeScores.BLUE = modeScores.BLUE + 1.5;
      modeScores.RED = Math.max(modeScores.RED - 1.0, 0.1);
    }
  }

  if (hasIncident && modeScores.INCIDENT !== undefined && modeScores.RED !== undefined) {
    // Incident context: boost INCIDENT, reduce RED
    modeScores.INCIDENT = modeScores.INCIDENT + 1.5;
    modeScores.RED = Math.max(modeScores.RED - 1.0, 0.1);
  }

  if (Object.keys(modeScores).length === 0) {
    return ['ARCHITECT', false];
  }

  // Find the top score and all modes that achieved it.
  const topScore = Math.max(...Object.values(modeScores));
  const topModes = Object.entries(modeScores)
    .filter(([, s]) => s >= topScore - 0.1)
    .map(([m]) => m);

  if (topModes.length === 1) {
    // BUILD layering: if BUILD scored strongly (>= 2) and another mode won,
    // return combined mode (e.g. "RED+BUILD")
    if (topModes[0] !== 'BUILD' && modeScores.BUILD >= 2) {
      return [`${topModes[0]}+BUILD`, false];
    }
    return [topModes[0], false];
  }

  // BUILD layering for tied modes: if BUILD is one of them, combine with the other
  if (topModes.length === 2 && topModes.includes('BUILD')) {
    const other = topModes.find(m => m !== 'BUILD');
    return [`${other}+BUILD`, false];
  }

  // Multiple modes tied: needs clarification.
  const sortedModes = topModes.sort().join(',');
  return [sortedModes, true];
}

/**
 * Remove [MODE: X] prefix from message text.
 *
 * Strips any leading whitespace after the prefix. Returns the original
 * string unchanged if no prefix is present.
 *
 * @param {string} message
 * @returns {string}
 */
export function stripModePrefix(message) {
  return message.replace(EXPLICIT_RE, '').trimStart();
}
