<?php

declare(strict_types=1);

namespace App\Framework\DDoS\Analysis;

use App\Framework\Cache\Cache;
use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\DateTime\Clock;
use App\Framework\Http\IpAddress;
use App\Framework\Logging\Logger;
use App\Infrastructure\GeoIp\GeoIp;

/**
 * GeoIP Anomaly Detector - Uses existing GeoIP infrastructure for geographic attack detection
 *
 * Detects distributed DDoS attacks by analyzing geographic patterns and anomalies
 * in request origins using the framework's existing GeoIP system.
 */
final class GeoAnomalyDetector
{
    private const string CACHE_PREFIX = 'geo_anomaly:';
    private const int BASELINE_PERIOD_HOURS = 24;
    private const int ANALYSIS_WINDOW_MINUTES = 15;
    private const float DIVERSITY_THRESHOLD = 0.8; // 80% country diversity = suspicious
    private const int MIN_REQUESTS_FOR_ANALYSIS = 10;

    // High-risk countries (often used in DDoS attacks)
    private const array HIGH_RISK_COUNTRIES = [
        'CN', 'RU', 'KP', 'IR', // Known for cyber activities
        'XX', 'ZZ', 'A1', 'A2',  // Anonymous/unknown
    ];

    // Countries with known bot farms or compromised infrastructure
    private const array BOT_FARM_COUNTRIES = [
        'CN', 'RU', 'VN', 'IN', 'BD', 'PK', 'UA', 'RO',
    ];

    public function __construct(
        private readonly Cache $cache,
        private readonly Clock $clock,
        private readonly Logger $logger
    ) {
    }

    /**
     * Detect geographic anomalies that may indicate distributed DDoS attacks
     */
    public function detectAnomalies(
        string $clientIp,
        GeoIp $geoIpService,
        Cache $cache
    ): array {
        $analysisStart = $this->clock->time();

        try {
            $ipAddress = IpAddress::from($clientIp);

            // Skip analysis for private IPs
            if ($ipAddress->isPrivate() || $ipAddress->isLoopback()) {
                return $this->createSafeResult('private_ip');
            }

            // Get country information
            $countryInfo = $geoIpService->getCountryInfoForIp($ipAddress);
            $countryCode = $countryInfo->getCountryCode();

            if (! $countryCode) {
                return $this->createSafeResult('unknown_country');
            }

            // Perform multi-dimensional geographic analysis
            $analyses = [
                'country_analysis' => $this->analyzeCountryRisk($countryCode),
                'distribution_analysis' => $this->analyzeGeographicDistribution($countryCode, $cache),
                'velocity_analysis' => $this->analyzeCountryVelocity($countryCode, $cache),
                'reputation_analysis' => $this->analyzeCountryReputation($countryCode, $cache),
                'pattern_analysis' => $this->analyzeAttackPatterns($countryCode, $clientIp, $cache),
            ];

            // Calculate combined anomaly score
            $anomalyScore = $this->calculateCombinedAnomalyScore($analyses);
            $confidence = $this->calculateAnalysisConfidence($analyses, $countryCode);

            // Update geographic baseline
            $this->updateGeographicBaseline($countryCode, $clientIp, $cache);

            $processingTime = $this->clock->time()->diff($analysisStart);

            return [
                'threat_score' => $anomalyScore,
                'confidence' => $confidence,
                'country_code' => $countryCode,
                'country_name' => $countryInfo->getEnglishName(),
                'is_suspicious' => $anomalyScore >= 0.7,
                'risk_factors' => $this->identifyRiskFactors($analyses, $countryCode),
                'analysis_breakdown' => $analyses,
                'processing_time' => $processingTime,
                'timestamp' => $this->clock->time(),
                'country_diversity' => $analyses['distribution_analysis']['diversity_score'] ?? 0.0,
            ];

        } catch (\Throwable $e) {
            $this->logger->error('GeoIP anomaly detection failed', [
                'client_ip' => $clientIp,
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);

            return $this->createSafeResult('analysis_failed');
        }
    }

    /**
     * Analyze country-specific risk factors
     */
    private function analyzeCountryRisk(string $countryCode): array
    {
        $riskScore = 0.0;
        $riskFactors = [];

        // High-risk country check
        if (in_array($countryCode, self::HIGH_RISK_COUNTRIES)) {
            $riskScore += 0.4;
            $riskFactors[] = 'high_risk_country';
        }

        // Bot farm country check
        if (in_array($countryCode, self::BOT_FARM_COUNTRIES)) {
            $riskScore += 0.3;
            $riskFactors[] = 'bot_farm_country';
        }

        // Anonymous/unknown country codes
        if (in_array($countryCode, ['XX', 'ZZ', 'A1', 'A2', ''])) {
            $riskScore += 0.5;
            $riskFactors[] = 'anonymous_country';
        }

        return [
            'risk_score' => min(1.0, $riskScore),
            'risk_factors' => $riskFactors,
            'country_code' => $countryCode,
            'is_high_risk' => $riskScore >= 0.5,
        ];
    }

    /**
     * Analyze geographic distribution patterns
     */
    private function analyzeGeographicDistribution(string $currentCountry, Cache $cache): array
    {
        $distributionKey = $this->generateKey('distribution', 'global');
        $timeWindow = Duration::fromMinutes(self::ANALYSIS_WINDOW_MINUTES);

        // Get recent country distribution
        $recentCountries = $cache->get($distributionKey, []);
        $now = $this->clock->time();
        $cutoffTime = $now->toFloat() - $timeWindow->toSeconds();

        // Filter recent entries and add current
        $recentCountries = array_filter($recentCountries, fn ($entry) => $entry['timestamp'] > $cutoffTime);
        $recentCountries[] = [
            'country' => $currentCountry,
            'timestamp' => $now->toFloat(),
        ];

        // Calculate diversity metrics
        $countryFrequency = array_count_values(array_column($recentCountries, 'country'));
        $totalRequests = count($recentCountries);
        $uniqueCountries = count($countryFrequency);

        // Diversity score (Shannon entropy)
        $diversityScore = $this->calculateShannonEntropy($countryFrequency, $totalRequests);

        // Geographic spread analysis
        $dominantCountry = array_keys($countryFrequency, max($countryFrequency))[0];
        $dominanceRatio = $countryFrequency[$dominantCountry] / $totalRequests;

        // High diversity with low dominance = distributed attack
        $isDistributed = $diversityScore > self::DIVERSITY_THRESHOLD && $dominanceRatio < 0.3;

        // Store updated distribution
        $cache->set($distributionKey, $recentCountries, $timeWindow->multiply(2)->toCacheSeconds());

        return [
            'diversity_score' => $diversityScore,
            'unique_countries' => $uniqueCountries,
            'total_requests' => $totalRequests,
            'dominant_country' => $dominantCountry,
            'dominance_ratio' => $dominanceRatio,
            'is_distributed' => $isDistributed,
            'country_frequency' => $countryFrequency,
            'distribution_anomaly_score' => $this->calculateDistributionAnomalyScore($diversityScore, $dominanceRatio),
        ];
    }

    /**
     * Analyze velocity of requests from specific country
     */
    private function analyzeCountryVelocity(string $countryCode, Cache $cache): array
    {
        $velocityKey = $this->generateKey('velocity', $countryCode);
        $timeWindow = Duration::fromMinutes(5); // 5-minute window

        $velocityData = $cache->get($velocityKey, ['timestamps' => [], 'baseline' => 0.0]);
        $now = $this->clock->time();
        $cutoffTime = $now->toFloat() - $timeWindow->toSeconds();

        // Add current timestamp and filter old ones
        $velocityData['timestamps'][] = $now->toFloat();
        $velocityData['timestamps'] = array_filter(
            $velocityData['timestamps'],
            fn ($ts) => $ts > $cutoffTime
        );

        $currentVelocity = count($velocityData['timestamps']) / $timeWindow->toMinutes();
        $baseline = $velocityData['baseline'];

        // Update baseline (exponential moving average)
        $velocityData['baseline'] = ($baseline * 0.9) + ($currentVelocity * 0.1);

        // Calculate velocity anomaly
        $velocityRatio = $baseline > 0 ? $currentVelocity / $baseline : 1.0;
        $isVelocityAnomaly = $velocityRatio > 5.0; // 5x normal velocity

        $cache->set($velocityKey, $velocityData, Duration::fromHours(1)->toCacheSeconds());

        return [
            'current_velocity' => $currentVelocity,
            'baseline_velocity' => $baseline,
            'velocity_ratio' => $velocityRatio,
            'is_velocity_anomaly' => $isVelocityAnomaly,
            'velocity_score' => min(1.0, $velocityRatio / 10.0), // Cap at 10x
        ];
    }

    /**
     * Analyze country reputation based on historical attacks
     */
    private function analyzeCountryReputation(string $countryCode, Cache $cache): array
    {
        $reputationKey = $this->generateKey('reputation', $countryCode);
        $reputation = $cache->get($reputationKey, [
            'attack_count' => 0,
            'total_requests' => 0,
            'last_attack' => 0,
            'reputation_score' => 0.5, // Neutral starting point
        ]);

        // Increment total requests
        $reputation['total_requests']++;

        // Calculate reputation based on attack ratio
        $attackRatio = $reputation['total_requests'] > 0 ?
            $reputation['attack_count'] / $reputation['total_requests'] : 0;

        // Time-based reputation decay (attacks become less relevant over time)
        $timeSinceLastAttack = $this->clock->time()->toFloat() - $reputation['last_attack'];
        $decayFactor = max(0.1, exp(-$timeSinceLastAttack / (86400 * 7))); // Week-based decay

        $reputation['reputation_score'] = ($attackRatio * $decayFactor * 0.8) + 0.2; // 0.2-1.0 range

        $cache->set($reputationKey, $reputation, Duration::fromDays(30)->toCacheSeconds());

        return [
            'reputation_score' => $reputation['reputation_score'],
            'attack_ratio' => $attackRatio,
            'attack_count' => $reputation['attack_count'],
            'total_requests' => $reputation['total_requests'],
            'is_bad_reputation' => $reputation['reputation_score'] > 0.7,
            'decay_factor' => $decayFactor,
        ];
    }

    /**
     * Analyze attack patterns specific to country/IP combinations
     */
    private function analyzeAttackPatterns(string $countryCode, string $clientIp, Cache $cache): array
    {
        $patternKey = $this->generateKey('patterns', $countryCode . ':' . $clientIp);
        $patterns = $cache->get($patternKey, [
            'request_intervals' => [],
            'user_agents' => [],
            'paths_accessed' => [],
        ]);

        $now = $this->clock->time()->toFloat();

        // Add current request data (simplified for this example)
        $patterns['request_intervals'][] = $now;

        // Keep only recent data (last hour)
        $oneHourAgo = $now - 3600;
        $patterns['request_intervals'] = array_filter(
            $patterns['request_intervals'],
            fn ($ts) => $ts > $oneHourAgo
        );

        // Analyze request timing patterns
        $intervals = $this->calculateRequestIntervals($patterns['request_intervals']);
        $regularityScore = $this->calculateTimingRegularity($intervals);

        // Bot-like behavior indicators
        $isBotLike = $regularityScore > 0.8 || count($patterns['request_intervals']) > 100;

        $cache->set($patternKey, $patterns, Duration::fromHours(2)->toCacheSeconds());

        return [
            'regularity_score' => $regularityScore,
            'request_count_1h' => count($patterns['request_intervals']),
            'is_bot_like' => $isBotLike,
            'pattern_anomaly_score' => ($regularityScore * 0.6) + ($isBotLike ? 0.4 : 0.0),
        ];
    }

    /**
     * Calculate combined anomaly score from all analyses
     */
    private function calculateCombinedAnomalyScore(array $analyses): float
    {
        $weights = [
            'country_analysis' => 0.25,      // Country risk factors
            'distribution_analysis' => 0.30, // Geographic distribution (primary indicator)
            'velocity_analysis' => 0.25,     // Request velocity anomalies
            'reputation_analysis' => 0.10,   // Historical reputation
            'pattern_analysis' => 0.10,       // Behavioral patterns
        ];

        $totalScore = 0.0;

        foreach ($analyses as $analysisType => $result) {
            $weight = $weights[$analysisType] ?? 0.0;

            $score = match ($analysisType) {
                'country_analysis' => $result['risk_score'] ?? 0.0,
                'distribution_analysis' => $result['distribution_anomaly_score'] ?? 0.0,
                'velocity_analysis' => $result['velocity_score'] ?? 0.0,
                'reputation_analysis' => $result['reputation_score'] ?? 0.0,
                'pattern_analysis' => $result['pattern_anomaly_score'] ?? 0.0,
                default => 0.0
            };

            $totalScore += $score * $weight;
        }

        return min(1.0, $totalScore);
    }

    /**
     * Calculate analysis confidence based on data quality
     */
    private function calculateAnalysisConfidence(array $analyses, string $countryCode): float
    {
        $confidenceFactors = [];

        // Known country = higher confidence
        $confidenceFactors[] = $countryCode !== 'XX' ? 0.9 : 0.3;

        // Sample size confidence
        $totalRequests = $analyses['distribution_analysis']['total_requests'] ?? 0;
        $sampleConfidence = min(1.0, $totalRequests / self::MIN_REQUESTS_FOR_ANALYSIS);
        $confidenceFactors[] = $sampleConfidence;

        // Data freshness confidence
        $confidenceFactors[] = 0.8; // Assume reasonably fresh data

        // Consistency across analysis layers
        $highRiskLayers = 0;
        foreach ($analyses as $analysis) {
            if (is_array($analysis) && isset($analysis['is_high_risk']) && $analysis['is_high_risk']) {
                $highRiskLayers++;
            }
        }
        $consistencyConfidence = $highRiskLayers / count($analyses);
        $confidenceFactors[] = $consistencyConfidence;

        return array_sum($confidenceFactors) / count($confidenceFactors);
    }

    /**
     * Identify specific risk factors for reporting
     */
    private function identifyRiskFactors(array $analyses, string $countryCode): array
    {
        $riskFactors = [];

        // Country-based risks
        if ($analyses['country_analysis']['is_high_risk'] ?? false) {
            $riskFactors = array_merge($riskFactors, $analyses['country_analysis']['risk_factors']);
        }

        // Distribution risks
        if ($analyses['distribution_analysis']['is_distributed'] ?? false) {
            $riskFactors[] = 'distributed_attack_pattern';
        }

        // Velocity risks
        if ($analyses['velocity_analysis']['is_velocity_anomaly'] ?? false) {
            $riskFactors[] = 'velocity_anomaly';
        }

        // Reputation risks
        if ($analyses['reputation_analysis']['is_bad_reputation'] ?? false) {
            $riskFactors[] = 'poor_country_reputation';
        }

        // Pattern risks
        if ($analyses['pattern_analysis']['is_bot_like'] ?? false) {
            $riskFactors[] = 'bot_like_behavior';
        }

        return array_unique($riskFactors);
    }

    // Helper methods
    private function createSafeResult(string $reason): array
    {
        return [
            'threat_score' => 0.0,
            'confidence' => 0.0,
            'country_code' => null,
            'country_name' => null,
            'is_suspicious' => false,
            'risk_factors' => [],
            'reason' => $reason,
            'country_diversity' => 0.0,
            'timestamp' => $this->clock->time(),
        ];
    }

    private function generateKey(string $type, string $identifier): string
    {
        return self::CACHE_PREFIX . $type . ':' . hash('sha256', $identifier);
    }

    private function calculateShannonEntropy(array $frequency, int $total): float
    {
        if ($total <= 1) {
            return 0.0;
        }

        $entropy = 0.0;
        foreach ($frequency as $count) {
            if ($count > 0) {
                $probability = $count / $total;
                $entropy -= $probability * log($probability, 2);
            }
        }

        // Normalize to 0-1 range (max entropy for countries ≈ log2(195))
        return min(1.0, $entropy / 8.0);
    }

    private function calculateDistributionAnomalyScore(float $diversityScore, float $dominanceRatio): float
    {
        // High diversity + low dominance = distributed attack
        if ($diversityScore > self::DIVERSITY_THRESHOLD && $dominanceRatio < 0.3) {
            return 0.9;
        }

        // Medium diversity = suspicious
        if ($diversityScore > 0.6) {
            return 0.5;
        }

        return 0.1;
    }

    private function calculateRequestIntervals(array $timestamps): array
    {
        if (count($timestamps) < 2) {
            return [];
        }

        sort($timestamps);
        $intervals = [];

        for ($i = 1; $i < count($timestamps); $i++) {
            $intervals[] = $timestamps[$i] - $timestamps[$i - 1];
        }

        return $intervals;
    }

    private function calculateTimingRegularity(array $intervals): float
    {
        if (count($intervals) < 3) {
            return 0.0;
        }

        $avg = array_sum($intervals) / count($intervals);
        $variance = array_sum(array_map(fn ($x) => pow($x - $avg, 2), $intervals)) / count($intervals);
        $stdDev = sqrt($variance);

        // Regular timing = high score (low coefficient of variation)
        $coefficientOfVariation = $avg > 0 ? $stdDev / $avg : 1.0;

        return max(0.0, 1.0 - $coefficientOfVariation);
    }

    private function updateGeographicBaseline(string $countryCode, string $clientIp, Cache $cache): void
    {
        $baselineKey = $this->generateKey('baseline', 'geographic');
        $baseline = $cache->get($baselineKey, [
            'country_frequency' => [],
            'total_requests' => 0,
            'last_updated' => $this->clock->time()->toFloat(),
        ]);

        // Update frequency
        $baseline['country_frequency'][$countryCode] = ($baseline['country_frequency'][$countryCode] ?? 0) + 1;
        $baseline['total_requests']++;
        $baseline['last_updated'] = $this->clock->time()->toFloat();

        // Keep baseline manageable (rolling window)
        if ($baseline['total_requests'] > 10000) {
            // Decay all frequencies by 10%
            foreach ($baseline['country_frequency'] as $country => $count) {
                $baseline['country_frequency'][$country] = (int) ($count * 0.9);
            }
            $baseline['total_requests'] = (int) ($baseline['total_requests'] * 0.9);
        }

        $cache->set($baselineKey, $baseline, Duration::fromDays(7)->toCacheSeconds());
    }
}
