/**
 * AnomalyCorrelator -- Cross-agent anomaly correlation engine.
 *
 * Analyzes anomaly patterns across multiple agents to detect
 * coordinated attacks, cascading failures, and systemic issues
 * that individual AnomalyDetectors cannot identify alone.
 *
 * T-071: Cross-agent anomaly correlation.
 *
 * @connexum/ai-governance
 */
import * as fs from 'fs';
import * as path from 'path';
import { createLogger } from '../util/log.js';
const log = createLogger('anomaly-correlator');
export class AnomalyCorrelator {
    config;
    correlationFile;
    constructor(config) {
        this.config = {
            correlationWindowMs: 5 * 60 * 1000, // 5 min (DEV MODE -- production: 300000 / 5 min)
            minAgentsForCorrelation: 2,
            minAnomaliesForAlert: 3,
            ...config,
        };
        if (!fs.existsSync(config.dataDir)) {
            fs.mkdirSync(config.dataDir, { recursive: true });
        }
        this.correlationFile = path.join(config.dataDir, 'correlations.json');
    }
    /**
     * Analyze anomalies across all agents and return correlated patterns.
     */
    correlate(allAnomalies) {
        const now = Date.now();
        const windowStart = now - this.config.correlationWindowMs;
        const results = [];
        // Collect all recent anomalies across agents
        const recentByAgent = new Map();
        const allRecent = [];
        for (const [agent, anomalies] of allAnomalies) {
            const recent = anomalies.filter(a => new Date(a.timestamp).getTime() >= windowStart);
            if (recent.length > 0) {
                recentByAgent.set(agent, recent);
                allRecent.push(...recent);
            }
        }
        // Not enough agents with anomalies
        if (recentByAgent.size < this.config.minAgentsForCorrelation)
            return results;
        // Check for coordinated spike (same metric, multiple agents)
        results.push(...this.detectCoordinatedSpike(recentByAgent, windowStart, now));
        // Check for cascade failure (sequential anomalies across agents)
        results.push(...this.detectCascadeFailure(recentByAgent, windowStart, now));
        // Check for systemic deviation
        results.push(...this.detectSystemicDeviation(recentByAgent, allAnomalies.size, windowStart, now));
        // Check for resource exhaustion
        results.push(...this.detectResourceExhaustion(recentByAgent, windowStart, now));
        // Persist correlations
        if (results.length > 0) {
            this.persistCorrelations(results);
            log.warn('Cross-agent anomaly correlation detected', {
                patterns: results.length,
                agents: Array.from(recentByAgent.keys()),
            });
        }
        return results;
    }
    /**
     * Load all agent anomalies from the data directory.
     */
    loadAllAgentAnomalies() {
        const result = new Map();
        if (!fs.existsSync(this.config.dataDir))
            return result;
        const files = fs.readdirSync(this.config.dataDir)
            .filter(f => f.startsWith('anomalies-') && f.endsWith('.json'));
        for (const file of files) {
            const agentName = file.replace('anomalies-', '').replace('.json', '');
            try {
                const data = JSON.parse(fs.readFileSync(path.join(this.config.dataDir, file), 'utf-8'));
                if (Array.isArray(data)) {
                    result.set(agentName, data);
                }
            }
            catch {
                // Skip corrupted files
            }
        }
        return result;
    }
    /**
     * Get all stored correlations.
     */
    getCorrelations() {
        if (!fs.existsSync(this.correlationFile))
            return [];
        try {
            return JSON.parse(fs.readFileSync(this.correlationFile, 'utf-8'));
        }
        catch {
            return [];
        }
    }
    /**
     * Get statistics.
     */
    getStats() {
        const correlations = this.getCorrelations();
        const byPattern = {};
        const bySeverity = {};
        const agents = new Set();
        for (const c of correlations) {
            byPattern[c.pattern] = (byPattern[c.pattern] || 0) + 1;
            bySeverity[c.severity] = (bySeverity[c.severity] || 0) + 1;
            for (const a of c.agents)
                agents.add(a);
        }
        return {
            totalCorrelations: correlations.length,
            byPattern,
            bySeverity,
            uniqueAgents: agents.size,
        };
    }
    // --- Pattern detection ---
    detectCoordinatedSpike(byAgent, windowStart, windowEnd) {
        const results = [];
        // Group by metric across agents
        const metricAgents = new Map();
        for (const [agent, anomalies] of byAgent) {
            for (const a of anomalies) {
                const key = a.metric;
                const list = metricAgents.get(key) || [];
                list.push({ agent, anomaly: a });
                metricAgents.set(key, list);
            }
        }
        for (const [metric, entries] of metricAgents) {
            const uniqueAgents = new Set(entries.map(e => e.agent));
            if (uniqueAgents.size >= this.config.minAgentsForCorrelation) {
                results.push({
                    id: `corr_spike_${Date.now()}_${metric}`,
                    pattern: 'COORDINATED_SPIKE',
                    agents: Array.from(uniqueAgents),
                    anomalies: entries.map(e => e.anomaly),
                    severity: uniqueAgents.size >= 3 ? 'CRITICAL' : 'HIGH',
                    description: `${uniqueAgents.size} agents spiked on "${metric}" within correlation window`,
                    detectedAt: new Date().toISOString(),
                    windowStart: new Date(windowStart).toISOString(),
                    windowEnd: new Date(windowEnd).toISOString(),
                });
            }
        }
        return results;
    }
    detectCascadeFailure(byAgent, windowStart, windowEnd) {
        const results = [];
        // Sort all anomalies chronologically
        const allSorted = [];
        for (const [agent, anomalies] of byAgent) {
            for (const a of anomalies) {
                allSorted.push({ agent, anomaly: a });
            }
        }
        allSorted.sort((a, b) => new Date(a.anomaly.timestamp).getTime() - new Date(b.anomaly.timestamp).getTime());
        // Look for sequential anomalies across different agents (cascade pattern)
        // A cascade = anomalies in 3+ different agents within short succession
        if (allSorted.length >= 3) {
            const cascadeWindow = this.config.correlationWindowMs / 5; // Tighter window for cascade
            const cascadeAgents = new Set();
            const cascadeAnomalies = [];
            let cascadeStart = new Date(allSorted[0].anomaly.timestamp).getTime();
            for (const entry of allSorted) {
                const ts = new Date(entry.anomaly.timestamp).getTime();
                if (ts - cascadeStart > cascadeWindow) {
                    // Check if we have a cascade
                    if (cascadeAgents.size >= this.config.minAgentsForCorrelation && cascadeAnomalies.length >= 3) {
                        results.push({
                            id: `corr_cascade_${Date.now()}`,
                            pattern: 'CASCADE_FAILURE',
                            agents: Array.from(cascadeAgents),
                            anomalies: [...cascadeAnomalies],
                            severity: 'CRITICAL',
                            description: `Anomalies cascaded across ${cascadeAgents.size} agents sequentially`,
                            detectedAt: new Date().toISOString(),
                            windowStart: new Date(windowStart).toISOString(),
                            windowEnd: new Date(windowEnd).toISOString(),
                        });
                    }
                    // Reset
                    cascadeAgents.clear();
                    cascadeAnomalies.length = 0;
                    cascadeStart = ts;
                }
                cascadeAgents.add(entry.agent);
                cascadeAnomalies.push(entry.anomaly);
            }
            // Check final window
            if (cascadeAgents.size >= this.config.minAgentsForCorrelation && cascadeAnomalies.length >= 3) {
                results.push({
                    id: `corr_cascade_${Date.now()}_final`,
                    pattern: 'CASCADE_FAILURE',
                    agents: Array.from(cascadeAgents),
                    anomalies: [...cascadeAnomalies],
                    severity: 'CRITICAL',
                    description: `Anomalies cascaded across ${cascadeAgents.size} agents sequentially`,
                    detectedAt: new Date().toISOString(),
                    windowStart: new Date(windowStart).toISOString(),
                    windowEnd: new Date(windowEnd).toISOString(),
                });
            }
        }
        return results;
    }
    detectSystemicDeviation(byAgent, totalAgents, windowStart, windowEnd) {
        const results = [];
        // Systemic = majority of monitored agents have anomalies
        const threshold = Math.max(2, Math.ceil(totalAgents * 0.5));
        if (byAgent.size >= threshold) {
            const allAnomalies = [];
            for (const anomalies of byAgent.values()) {
                allAnomalies.push(...anomalies);
            }
            results.push({
                id: `corr_systemic_${Date.now()}`,
                pattern: 'SYSTEMIC_DEVIATION',
                agents: Array.from(byAgent.keys()),
                anomalies: allAnomalies,
                severity: 'CRITICAL',
                description: `${byAgent.size}/${totalAgents} agents showing anomalies (systemic issue)`,
                detectedAt: new Date().toISOString(),
                windowStart: new Date(windowStart).toISOString(),
                windowEnd: new Date(windowEnd).toISOString(),
            });
        }
        return results;
    }
    detectResourceExhaustion(byAgent, windowStart, windowEnd) {
        const results = [];
        const resourceMetrics = ['Token usage', 'Cycle duration'];
        const resourceAgents = new Set();
        const resourceAnomalies = [];
        for (const [agent, anomalies] of byAgent) {
            const resourceHits = anomalies.filter(a => resourceMetrics.includes(a.metric));
            if (resourceHits.length > 0) {
                resourceAgents.add(agent);
                resourceAnomalies.push(...resourceHits);
            }
        }
        if (resourceAgents.size >= this.config.minAgentsForCorrelation && resourceAnomalies.length >= this.config.minAnomaliesForAlert) {
            results.push({
                id: `corr_resource_${Date.now()}`,
                pattern: 'RESOURCE_EXHAUSTION',
                agents: Array.from(resourceAgents),
                anomalies: resourceAnomalies,
                severity: 'HIGH',
                description: `${resourceAgents.size} agents show resource exhaustion (token/duration spikes)`,
                detectedAt: new Date().toISOString(),
                windowStart: new Date(windowStart).toISOString(),
                windowEnd: new Date(windowEnd).toISOString(),
            });
        }
        return results;
    }
    persistCorrelations(newCorrelations) {
        try {
            const existing = this.getCorrelations();
            existing.push(...newCorrelations);
            // Keep last 200
            const trimmed = existing.slice(-200);
            fs.writeFileSync(this.correlationFile, JSON.stringify(trimmed, null, 2));
        }
        catch {
            log.error('Failed to persist correlations');
        }
    }
}
//# sourceMappingURL=anomaly-correlator.js.map