<?php

declare(strict_types=1);

namespace App\Framework\Waf\MachineLearning;

use App\Framework\Waf\MachineLearning\ValueObjects\BehaviorFeatures;
use App\Framework\Waf\MachineLearning\ValueObjects\BehaviorAnomalyResult;
use App\Framework\Core\ValueObjects\Score;

/**
 * ML-based Behavioral Anomaly Detector for WAF
 *
 * Detects anomalous request patterns using statistical analysis:
 * - Z-score based outlier detection
 * - IQR (Interquartile Range) method
 * - Pattern-based heuristics for known attack types
 *
 * Based on N+1 Detection ML Framework patterns with statistical detection
 */
final readonly class BehaviorAnomalyDetector
{
    /**
     * @param Score $anomalyThreshold Minimum score for anomaly classification (default: 0.6)
     * @param float $zScoreThreshold Z-score threshold for outlier detection (default: 3.0 = 99.7%)
     * @param float $iqrMultiplier IQR multiplier for outlier bounds (default: 1.5)
     */
    public function __construct(
        private Score $anomalyThreshold = new Score(0.6),
        private float $zScoreThreshold = 3.0,
        private float $iqrMultiplier = 1.5
    ) {}

    /**
     * Detect anomalies in behavioral features
     *
     * @param BehaviorFeatures $features Extracted behavioral features
     * @param array<BehaviorFeatures> $historicalBaseline Historical baseline for comparison (optional)
     * @return BehaviorAnomalyResult Anomaly detection result with confidence score
     */
    public function detect(BehaviorFeatures $features, array $historicalBaseline = []): BehaviorAnomalyResult
    {
        // 1. Heuristic-based detection (pattern matching)
        $heuristicResult = $this->detectHeuristicAnomalies($features);

        if ($heuristicResult->isAnomalous) {
            return $heuristicResult;
        }

        // 2. Statistical detection (if baseline available)
        if (!empty($historicalBaseline)) {
            $statisticalResult = $this->detectStatisticalAnomalies($features, $historicalBaseline);

            if ($statisticalResult->isAnomalous) {
                return $statisticalResult;
            }

            // Merge heuristic and statistical scores for low-confidence cases
            if ($statisticalResult->anomalyScore->isAbove(Score::low())) {
                return $heuristicResult->merge($statisticalResult, weight: 0.5);
            }
        }

        return BehaviorAnomalyResult::normal('Behavioral patterns within expected parameters');
    }

    /**
     * Heuristic-based anomaly detection using known attack patterns
     */
    private function detectHeuristicAnomalies(BehaviorFeatures $features): BehaviorAnomalyResult
    {
        $featureScores = [];
        $detectedPatterns = [];
        $scores = [];

        // Check for DDoS-like patterns: High frequency + Low diversity
        if ($features->requestFrequency > 10.0 && $features->endpointDiversity < 1.0) {
            $ddosScore = Score::high();
            $featureScores['ddos_pattern'] = $ddosScore;
            $scores[] = $ddosScore;
            $detectedPatterns[] = [
                'type' => 'potential_ddos',
                'frequency' => $features->requestFrequency,
                'diversity' => $features->endpointDiversity
            ];
        }

        // Check for scanning behavior: High entropy + Geographic anomaly
        if ($features->parameterEntropy > 6.0 && $features->geographicAnomaly > 0.7) {
            $scanScore = Score::high();
            $featureScores['scanning_pattern'] = $scanScore;
            $scores[] = $scanScore;
            $detectedPatterns[] = [
                'type' => 'potential_scanning',
                'entropy' => $features->parameterEntropy,
                'geo_anomaly' => $features->geographicAnomaly
            ];
        }

        // Check for bot behavior: Perfect regularity + High similarity
        if ($features->timePatternRegularity > 0.9 && $features->payloadSimilarity > 0.8) {
            $botScore = Score::medium();
            $featureScores['bot_pattern'] = $botScore;
            $scores[] = $botScore;
            $detectedPatterns[] = [
                'type' => 'potential_bot',
                'regularity' => $features->timePatternRegularity,
                'similarity' => $features->payloadSimilarity
            ];
        }

        // Check for credential stuffing: High frequency + Inconsistent User-Agent
        if ($features->requestFrequency > 5.0 && $features->userAgentConsistency < 0.3) {
            $credentialStuffingScore = Score::high();
            $featureScores['credential_stuffing'] = $credentialStuffingScore;
            $scores[] = $credentialStuffingScore;
            $detectedPatterns[] = [
                'type' => 'potential_credential_stuffing',
                'frequency' => $features->requestFrequency,
                'ua_consistency' => $features->userAgentConsistency
            ];
        }

        // Calculate overall anomaly score
        if (empty($scores)) {
            return BehaviorAnomalyResult::normal('No heuristic patterns detected');
        }

        $overallScore = Score::weightedAverage($scores);

        if ($overallScore->isBelow($this->anomalyThreshold)) {
            return BehaviorAnomalyResult::lowConfidence($overallScore, $featureScores);
        }

        return BehaviorAnomalyResult::anomalous(
            score: $overallScore,
            featureScores: $featureScores,
            detectedPatterns: $detectedPatterns,
            primaryIndicator: $this->determinePrimaryThreat($detectedPatterns)
        );
    }

    /**
     * Statistical anomaly detection using baseline comparison
     *
     * @param BehaviorFeatures $features Current features
     * @param array<BehaviorFeatures> $baseline Historical baseline
     */
    private function detectStatisticalAnomalies(
        BehaviorFeatures $features,
        array $baseline
    ): BehaviorAnomalyResult {
        $featureScores = [];
        $detectedAnomalies = [];

        // Analyze each feature dimension
        $featureNames = BehaviorFeatures::getFeatureNames();
        $currentVector = $features->toVector();

        foreach ($featureNames as $index => $featureName) {
            $currentValue = $currentVector[$index];

            // Extract historical values for this feature
            $historicalValues = array_map(
                fn(BehaviorFeatures $f) => $f->toVector()[$index],
                $baseline
            );

            // Z-score based detection
            $zScore = $this->calculateZScore($currentValue, $historicalValues);

            if (abs($zScore) > $this->zScoreThreshold) {
                $anomalyScore = $this->zScoreToConfidence(abs($zScore));
                $featureScores[$featureName] = $anomalyScore;

                $detectedAnomalies[] = [
                    'feature' => $featureName,
                    'type' => 'statistical_outlier',
                    'current_value' => $currentValue,
                    'z_score' => $zScore,
                    'confidence' => $anomalyScore->value()
                ];
            }

            // IQR-based detection
            $iqrOutlier = $this->isIQROutlier($currentValue, $historicalValues);

            if ($iqrOutlier && !isset($featureScores[$featureName])) {
                $iqrScore = Score::medium();
                $featureScores[$featureName] = $iqrScore;

                $detectedAnomalies[] = [
                    'feature' => $featureName,
                    'type' => 'iqr_outlier',
                    'current_value' => $currentValue
                ];
            }
        }

        if (empty($featureScores)) {
            return BehaviorAnomalyResult::normal('Statistical analysis: within baseline parameters');
        }

        // Calculate overall anomaly score as weighted average of feature scores
        $overallScore = Score::weightedAverage(array_values($featureScores));

        if ($overallScore->isBelow($this->anomalyThreshold)) {
            return BehaviorAnomalyResult::lowConfidence($overallScore, $featureScores);
        }

        return BehaviorAnomalyResult::anomalous(
            score: $overallScore,
            featureScores: $featureScores,
            detectedPatterns: $detectedAnomalies,
            primaryIndicator: 'Statistical deviation from baseline behavior'
        );
    }

    /**
     * Calculate Z-score for a value given historical distribution
     */
    private function calculateZScore(float $value, array $historicalValues): float
    {
        if (empty($historicalValues)) {
            return 0.0;
        }

        $mean = array_sum($historicalValues) / count($historicalValues);
        $variance = 0.0;

        foreach ($historicalValues as $histValue) {
            $variance += ($histValue - $mean) ** 2;
        }

        $stdDev = sqrt($variance / count($historicalValues));

        if ($stdDev === 0.0) {
            return 0.0; // No variation in baseline
        }

        return ($value - $mean) / $stdDev;
    }

    /**
     * Check if value is an IQR outlier
     */
    private function isIQROutlier(float $value, array $historicalValues): bool
    {
        if (count($historicalValues) < 4) {
            return false; // Need at least 4 values for quartiles
        }

        sort($historicalValues);

        $q1Index = (int) floor(count($historicalValues) * 0.25);
        $q3Index = (int) floor(count($historicalValues) * 0.75);

        $q1 = $historicalValues[$q1Index];
        $q3 = $historicalValues[$q3Index];

        $iqr = $q3 - $q1;

        $lowerBound = $q1 - ($this->iqrMultiplier * $iqr);
        $upperBound = $q3 + ($this->iqrMultiplier * $iqr);

        return $value < $lowerBound || $value > $upperBound;
    }

    /**
     * Convert Z-score to confidence score (0.0-1.0)
     *
     * Maps Z-score to confidence using sigmoid-like function
     */
    private function zScoreToConfidence(float $absZScore): Score
    {
        // Z-score thresholds:
        // 3.0 (99.7%) -> 0.7 (high confidence)
        // 4.0 -> 0.85
        // 5.0+ -> 0.95 (critical)

        $confidence = match (true) {
            $absZScore >= 5.0 => 0.95,
            $absZScore >= 4.0 => 0.85,
            $absZScore >= 3.0 => 0.70,
            $absZScore >= 2.0 => 0.50,
            default => 0.30
        };

        return new Score($confidence);
    }

    /**
     * Determine primary threat from detected patterns
     */
    private function determinePrimaryThreat(array $detectedPatterns): string
    {
        if (empty($detectedPatterns)) {
            return 'Unknown threat';
        }

        // Priority order: DDoS > Credential Stuffing > Scanning > Bot
        $priorities = [
            'potential_ddos' => 4,
            'potential_credential_stuffing' => 3,
            'potential_scanning' => 2,
            'potential_bot' => 1
        ];

        $maxPriority = 0;
        $primaryThreat = 'Unknown threat';

        foreach ($detectedPatterns as $pattern) {
            $type = $pattern['type'];
            $priority = $priorities[$type] ?? 0;

            if ($priority > $maxPriority) {
                $maxPriority = $priority;
                $primaryThreat = ucfirst(str_replace('potential_', '', $type)) . ' detected';
            }
        }

        return $primaryThreat;
    }

    /**
     * Get detector configuration
     */
    public function getConfiguration(): array
    {
        return [
            'anomaly_threshold' => $this->anomalyThreshold->toArray(),
            'z_score_threshold' => $this->zScoreThreshold,
            'iqr_multiplier' => $this->iqrMultiplier
        ];
    }
}
