<?php

declare(strict_types=1);

namespace App\Framework\Waf\Analysis\Analyzers;

use App\Framework\Core\ValueObjects\Timestamp;
use App\Framework\Waf\Analysis\AnalysisResult;
use App\Framework\Waf\Analysis\AnalysisType;
use App\Framework\Waf\Analysis\ValueObjects\RequestAnalysisData;
use App\Framework\Waf\DetectionSeverity;

/**
 * Cookie Security Analyzer
 * Analyzes HTTP cookies for security threats and compliance
 */
final class CookieAnalyzer implements AnalyzerInterface
{
    public function __construct(
        private readonly bool $enabled = true,
        private readonly int $maxCookieCount = 100,
        private readonly int $maxCookieSize = 4096,
        private readonly int $maxCookieNameLength = 256,
        private readonly int $maxTotalCookieSize = 32768, // 32KB
        private readonly array $suspiciousCookieNames = [
            'admin', 'root', 'user', 'password', 'pass', 'auth', 'token',
            'session', 'sess', 'login', 'logged', 'authenticated',
            'debug', 'test', 'dev', 'development', 'config', 'settings',
            'cmd', 'command', 'exec', 'system', 'shell', 'eval',
            'sql', 'query', 'database', 'db',
        ],
        private readonly array $requiredSecurityAttributes = [
            'HttpOnly', 'Secure', 'SameSite',
        ],
        private readonly array $injectionPatterns = [
            // XSS patterns in cookies
            'xss' => [
                '/(?i:<script[^>]*>.*?<\\/script>)/',
                '/(?i:on(?:load|error|click|focus|blur|change|submit|reset|select|keydown|keyup|keypress|mouseover|mouseout|mousedown|mouseup|mousemove)[\\s]*=)/',
                '/(?i:javascript[\\s]*:)/',
                '/(?i:vbscript[\\s]*:)/',
                '/(?i:data[\\s]*:[^;]*;base64)/',
            ],
            // SQL injection patterns
            'sql_injection' => [
                '/(?i:union[\\s\\/\\*]+(?:all[\\s\\/\\*]+)?select)/',
                '/(?i:[\\\'\\"`][\\s]*(?:or|and)[\\s]*[\\\'\\"`]*[\\s]*(?:[\\\'\\"`]*[\\w]+[\\\'\\"`]*[\\s]*=[\\s]*[\\\'\\"`]*[\\w]+|[\\d]+[\\s]*=[\\s]*[\\d]+))/',
                '/(?i:(?:--|#|\\/\\*|\\*\\/))/',
            ],
            // Command injection
            'command_injection' => [
                '/(?i:(?:;|\\||\\|\\||&&|&|`|\\$\\(|\\${)[\\s]*(?:cat|ls|pwd|id|uname|whoami|ps|kill|rm|mv|cp|chmod|chown))/',
                '/(?i:(?:cmd|command)\\.exe|powershell|bash|sh)[\\s]*(?:\\/c|\\/k|-c|-e)/',
            ],
            // Path traversal
            'path_traversal' => [
                '/(?i:(?:\\.\\.[\\\\/])|(?:[\\\\/]\\.\\.)|(?:\\.\\.\\\\\\\\)|(?:\\\\\\\\\\.\\.))/',
                '/(?i:(?:\\/etc\\/passwd|\\/etc\\/shadow|\\/proc\\/|\\/sys\\/|c:[\\\\\\\\\\\\/]))/',
            ],
        ]
    ) {
    }

    public function getType(): AnalysisType
    {
        return AnalysisType::COOKIES;
    }

    public function canAnalyze(RequestAnalysisData $requestData): bool
    {
        return ! empty($requestData->cookies);
    }

    public function analyze(RequestAnalysisData $requestData): AnalysisResult
    {
        $startTime = Timestamp::now();
        $findings = [];
        $metadata = [];
        $severity = DetectionSeverity::INFO;
        $message = 'Cookie analysis completed';

        $cookieCount = count($requestData->cookies);
        $metadata['cookie_count'] = $cookieCount;

        // Check cookie count limits
        if ($cookieCount > $this->maxCookieCount) {
            $findings[] = [
                'type' => 'excessive_cookies',
                'message' => "Too many cookies: {$cookieCount} (max: {$this->maxCookieCount})",
                'severity' => 'medium',
                'count' => $cookieCount,
                'limit' => $this->maxCookieCount,
            ];
            $severity = DetectionSeverity::MEDIUM;
            $message = 'Excessive number of cookies detected';
        }

        $totalCookieSize = 0;
        $suspiciousCookieCount = 0;
        $encodedCookieCount = 0;
        $insecureCookieCount = 0;
        $duplicateCookieCount = 0;

        // Track cookie names for duplicate detection
        $cookieNames = [];

        foreach ($requestData->cookies as $name => $value) {
            $cookieSize = strlen($name) + strlen($value);
            $totalCookieSize += $cookieSize;

            // Track cookie names (case-insensitive)
            $normalizedName = strtolower($name);
            if (isset($cookieNames[$normalizedName])) {
                $cookieNames[$normalizedName]++;
                $duplicateCookieCount++;
            } else {
                $cookieNames[$normalizedName] = 1;
            }

            // Check cookie name length
            if (strlen($name) > $this->maxCookieNameLength) {
                $findings[] = [
                    'type' => 'oversized_cookie_name',
                    'message' => "Cookie name too long: '{$name}' ({strlen($name)} chars)",
                    'severity' => 'medium',
                    'cookie' => $name,
                    'length' => strlen($name),
                    'limit' => $this->maxCookieNameLength,
                ];
                if ($severity === DetectionSeverity::INFO || $severity === DetectionSeverity::LOW) {
                    $severity = DetectionSeverity::MEDIUM;
                }
            }

            // Check individual cookie size
            if ($cookieSize > $this->maxCookieSize) {
                $findings[] = [
                    'type' => 'oversized_cookie',
                    'message' => "Cookie '{$name}' is too large: {$cookieSize} bytes",
                    'severity' => 'medium',
                    'cookie' => $name,
                    'size' => $cookieSize,
                    'limit' => $this->maxCookieSize,
                ];
                if ($severity === DetectionSeverity::INFO || $severity === DetectionSeverity::LOW) {
                    $severity = DetectionSeverity::MEDIUM;
                }
            }

            // Check for suspicious cookie names
            if ($this->isSuspiciousCookieName($name)) {
                $suspiciousCookieCount++;
                $findings[] = [
                    'type' => 'suspicious_cookie_name',
                    'message' => "Suspicious cookie name: '{$name}'",
                    'severity' => 'low',
                    'cookie' => $name,
                    'value' => substr($value, 0, 100), // Truncate for safety
                ];
                if ($severity === DetectionSeverity::INFO) {
                    $severity = DetectionSeverity::LOW;
                }
            }

            // Check for injection attacks in cookie values
            $injectionResults = $this->checkForInjections($name, $value);
            if (! empty($injectionResults)) {
                $findings = array_merge($findings, $injectionResults);
                $maxInjectionSeverity = $this->getMaxSeverityFromFindings($injectionResults);
                if ($maxInjectionSeverity->getCvssScore() > $severity->getCvssScore()) {
                    $severity = $maxInjectionSeverity;
                }
            }

            // Check for encoded content
            if ($this->hasEncodedContent($value)) {
                $encodedCookieCount++;
            }

            // Check for null bytes
            if (str_contains($value, "\x00")) {
                $findings[] = [
                    'type' => 'null_byte_cookie',
                    'message' => "Null byte in cookie '{$name}'",
                    'severity' => 'high',
                    'cookie' => $name,
                    'count' => substr_count($value, "\x00"),
                ];
                $severity = DetectionSeverity::HIGH;
            }

            // Check for control characters
            if (preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $value)) {
                $findings[] = [
                    'type' => 'control_characters_cookie',
                    'message' => "Control characters in cookie '{$name}'",
                    'severity' => 'medium',
                    'cookie' => $name,
                ];
                if ($severity === DetectionSeverity::INFO || $severity === DetectionSeverity::LOW) {
                    $severity = DetectionSeverity::MEDIUM;
                }
            }

            // Check for CRLF injection in cookie values
            if (preg_match('/[\\r\\n]/', $value)) {
                $findings[] = [
                    'type' => 'crlf_injection_cookie',
                    'message' => "CRLF injection in cookie '{$name}'",
                    'severity' => 'high',
                    'cookie' => $name,
                ];
                $severity = DetectionSeverity::HIGH;
            }

            // Check for session hijacking indicators
            if ($this->isSessionCookie($name) && $this->isSuspiciousSessionValue($value)) {
                $findings[] = [
                    'type' => 'suspicious_session_cookie',
                    'message' => "Suspicious session cookie value: '{$name}'",
                    'severity' => 'medium',
                    'cookie' => $name,
                    'reason' => 'Potentially manipulated session identifier',
                ];
                if ($severity === DetectionSeverity::INFO || $severity === DetectionSeverity::LOW) {
                    $severity = DetectionSeverity::MEDIUM;
                }
            }

            // Check for authentication bypass attempts
            if ($this->isAuthenticationCookie($name) && $this->isPrivilegeEscalationAttempt($value)) {
                $findings[] = [
                    'type' => 'privilege_escalation_cookie',
                    'message' => "Privilege escalation attempt in cookie '{$name}'",
                    'severity' => 'high',
                    'cookie' => $name,
                ];
                $severity = DetectionSeverity::HIGH;
            }
        }

        // Check total cookie size
        if ($totalCookieSize > $this->maxTotalCookieSize) {
            $findings[] = [
                'type' => 'excessive_total_cookie_size',
                'message' => "Total cookie size too large: {$totalCookieSize} bytes (max: {$this->maxTotalCookieSize})",
                'severity' => 'medium',
                'total_size' => $totalCookieSize,
                'limit' => $this->maxTotalCookieSize,
            ];
            if ($severity === DetectionSeverity::INFO || $severity === DetectionSeverity::LOW) {
                $severity = DetectionSeverity::MEDIUM;
            }
        }

        // Check for cookie security compliance
        $securityResults = $this->analyzeSecurityCompliance($requestData);
        if (! empty($securityResults)) {
            $findings = array_merge($findings, $securityResults);
            $insecureCookieCount = count($securityResults);
        }

        // Check for cookie tampering patterns
        $tamperingResults = $this->analyzeCookieTampering($requestData->cookies);
        if (! empty($tamperingResults)) {
            $findings = array_merge($findings, $tamperingResults);
        }

        // Report duplicate cookies
        if ($duplicateCookieCount > 0) {
            $findings[] = [
                'type' => 'duplicate_cookies',
                'message' => "Duplicate cookie names detected: {$duplicateCookieCount} duplicates",
                'severity' => 'low',
                'count' => $duplicateCookieCount,
            ];
            if ($severity === DetectionSeverity::INFO) {
                $severity = DetectionSeverity::LOW;
            }
        }

        $endTime = Timestamp::now();
        $processingTime = $startTime->diff($endTime);

        $passed = empty($findings) ||
                  ! in_array($severity, [DetectionSeverity::HIGH, DetectionSeverity::CRITICAL]);

        if (! $passed) {
            $criticalFindings = array_filter($findings, fn ($f) => $f['severity'] === 'critical');
            $highFindings = array_filter($findings, fn ($f) => $f['severity'] === 'high');

            if (! empty($criticalFindings)) {
                $message = count($criticalFindings) === 1
                    ? $criticalFindings[0]['message']
                    : "Critical security threats detected in cookies (" . count($criticalFindings) . " threats)";
            } elseif (! empty($highFindings)) {
                $message = count($highFindings) === 1
                    ? $highFindings[0]['message']
                    : "High-severity security issues detected in cookies (" . count($highFindings) . " issues)";
            } else {
                $message = "Security issues detected in cookies (" . count($findings) . " findings)";
            }
        }

        $metadata['total_cookie_size'] = $totalCookieSize;
        $metadata['suspicious_cookie_count'] = $suspiciousCookieCount;
        $metadata['encoded_cookie_count'] = $encodedCookieCount;
        $metadata['insecure_cookie_count'] = $insecureCookieCount;
        $metadata['duplicate_cookie_count'] = $duplicateCookieCount;
        $metadata['analysis_duration_ms'] = $processingTime->toMilliseconds();
        $metadata['analyzer'] = 'CookieAnalyzer';

        return (new AnalysisResult(
            type: $this->getType(),
            passed: $passed,
            severity: $severity,
            message: $message,
            findings: $findings,
            metadata: $metadata
        ))->withProcessingTime($processingTime);
    }

    /**
     * Check if cookie name is suspicious
     */
    private function isSuspiciousCookieName(string $name): bool
    {
        $normalizedName = strtolower($name);

        foreach ($this->suspiciousCookieNames as $suspicious) {
            if (str_contains($normalizedName, $suspicious)) {
                return true;
            }
        }

        // Check for encoded cookie names
        if (preg_match('/%[0-9a-fA-F]{2}/', $name)) {
            return true;
        }

        // Check for unusual characters in cookie names
        if (preg_match('/[^a-zA-Z0-9_-]/', $name)) {
            return true;
        }

        return false;
    }

    /**
     * Check for injection attacks in cookie value
     */
    private function checkForInjections(string $cookieName, string $value): array
    {
        $findings = [];

        foreach ($this->injectionPatterns as $category => $patterns) {
            foreach ($patterns as $pattern) {
                if (preg_match($pattern, $value, $matches, PREG_OFFSET_CAPTURE)) {
                    $matchText = $matches[0][0];
                    $matchOffset = $matches[0][1];

                    $severity = $this->getSeverityForInjectionType($category);

                    $findings[] = [
                        'type' => $category . '_cookie',
                        'message' => "Potential {$category} in cookie '{$cookieName}'",
                        'severity' => $severity,
                        'cookie' => $cookieName,
                        'pattern' => $pattern,
                        'match' => substr($matchText, 0, 100), // Truncate for safety
                        'offset' => $matchOffset,
                        'category' => $category,
                    ];
                }
            }
        }

        return $findings;
    }

    /**
     * Check if value contains encoded content
     */
    private function hasEncodedContent(string $value): bool
    {
        // Check for URL encoding
        $urlEncodedMatches = preg_match_all('/%[0-9a-fA-F]{2}/', $value);
        if ($urlEncodedMatches > 3) {
            return true;
        }

        // Check for Base64 encoding
        if (preg_match('/^[A-Za-z0-9+\/]{20,}={0,2}$/', trim($value))) {
            return true;
        }

        // Check for HTML entity encoding
        $htmlEntityMatches = preg_match_all('/&(?:#[0-9]+|#x[0-9a-fA-F]+|[a-zA-Z][a-zA-Z0-9]*);/', $value);
        if ($htmlEntityMatches > 2) {
            return true;
        }

        return false;
    }

    /**
     * Check if cookie is a session cookie
     */
    private function isSessionCookie(string $name): bool
    {
        $sessionPatterns = [
            'session', 'sess', 'sid', 'sessionid', 'session_id',
            'phpsessid', 'jsessionid', 'aspsessionid',
        ];

        $normalizedName = strtolower($name);

        foreach ($sessionPatterns as $pattern) {
            if (str_contains($normalizedName, $pattern)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check if session value is suspicious
     */
    private function isSuspiciousSessionValue(string $value): bool
    {
        // Check for predictable session IDs
        if (preg_match('/^(0+|1+|admin|test|guest)$/', strtolower($value))) {
            return true;
        }

        // Check for short session IDs (potential brute force)
        if (strlen($value) < 16) {
            return true;
        }

        // Check for sequential patterns
        if (preg_match('/^(123|abc|000|111|aaa)/i', $value)) {
            return true;
        }

        return false;
    }

    /**
     * Check if cookie is authentication-related
     */
    private function isAuthenticationCookie(string $name): bool
    {
        $authPatterns = [
            'auth', 'token', 'login', 'user', 'admin', 'role', 'permission',
        ];

        $normalizedName = strtolower($name);

        foreach ($authPatterns as $pattern) {
            if (str_contains($normalizedName, $pattern)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check for privilege escalation attempts
     */
    private function isPrivilegeEscalationAttempt(string $value): bool
    {
        $privilegePatterns = [
            'admin', 'root', 'administrator', 'superuser', 'su',
            'role=admin', 'user=admin', 'level=admin', 'type=admin',
        ];

        $normalizedValue = strtolower($value);

        foreach ($privilegePatterns as $pattern) {
            if (str_contains($normalizedValue, $pattern)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Analyze cookie security compliance
     */
    private function analyzeSecurityCompliance(RequestAnalysisData $requestData): array
    {
        $findings = [];

        // Note: This analysis is limited since we only have cookie values,
        // not the full Set-Cookie headers with security attributes.
        // In a real implementation, this would need access to response headers.

        foreach ($requestData->cookies as $name => $value) {
            // Check for secure cookies being sent over non-HTTPS
            if ($this->isSecuritySensitiveCookie($name) && ! $this->isHttpsRequest($requestData)) {
                $findings[] = [
                    'type' => 'insecure_transport',
                    'message' => "Security-sensitive cookie '{$name}' sent over non-HTTPS",
                    'severity' => 'medium',
                    'cookie' => $name,
                    'recommendation' => 'Use HTTPS for all security-sensitive cookies',
                ];
            }

            // Check for potential session fixation
            if ($this->isSessionCookie($name) && strlen($value) > 0) {
                // Simple heuristic: check if session ID looks manually crafted
                if (! ctype_alnum($value) && ! preg_match('/^[a-zA-Z0-9+\/]+=*$/', $value)) {
                    $findings[] = [
                        'type' => 'potential_session_fixation',
                        'message' => "Potentially fixed session ID in cookie '{$name}'",
                        'severity' => 'medium',
                        'cookie' => $name,
                        'value' => substr($value, 0, 20) . '...',
                    ];
                }
            }
        }

        return $findings;
    }

    /**
     * Analyze cookie tampering patterns
     */
    private function analyzeCookieTampering(array $cookies): array
    {
        $findings = [];

        foreach ($cookies as $name => $value) {
            // Check for timestamp manipulation
            if (preg_match('/(?:time|timestamp|expires?|date)[^=]*=(\d+)/', $value, $matches)) {
                $timestamp = (int) $matches[1];
                $currentTime = time();

                // Check for future timestamps (possible tampering)
                if ($timestamp > $currentTime + 86400) { // More than 1 day in future
                    $findings[] = [
                        'type' => 'future_timestamp',
                        'message' => "Future timestamp in cookie '{$name}' (possible tampering)",
                        'severity' => 'medium',
                        'cookie' => $name,
                        'timestamp' => $timestamp,
                        'current_time' => $currentTime,
                    ];
                }
            }

            // Check for signature/hash manipulation indicators
            if (preg_match('/(?:sig|signature|hash|hmac|checksum)[^=]*=([^&;]+)/', $value, $matches)) {
                $signature = $matches[1];

                // Simple checks for obviously tampered signatures
                if (strlen($signature) < 16 || $signature === 'null' || $signature === 'undefined') {
                    $findings[] = [
                        'type' => 'tampered_signature',
                        'message' => "Potentially tampered signature in cookie '{$name}'",
                        'severity' => 'high',
                        'cookie' => $name,
                        'signature' => $signature,
                    ];
                }
            }
        }

        return $findings;
    }

    /**
     * Check if cookie is security-sensitive
     */
    private function isSecuritySensitiveCookie(string $name): bool
    {
        return $this->isSessionCookie($name) || $this->isAuthenticationCookie($name);
    }

    /**
     * Check if request is over HTTPS
     */
    private function isHttpsRequest(RequestAnalysisData $requestData): bool
    {
        // Check for HTTPS indicators in headers
        $httpsIndicators = [
            'X-Forwarded-Proto' => 'https',
            'X-Forwarded-SSL' => 'on',
            'X-Forwarded-Scheme' => 'https',
        ];

        foreach ($httpsIndicators as $header => $value) {
            $headerValue = $requestData->getHeader($header);
            if ($headerValue !== null && strtolower($headerValue) === $value) {
                return true;
            }
        }

        // Check protocol
        return $requestData->protocol !== null && str_starts_with(strtolower($requestData->protocol), 'https');
    }

    /**
     * Get severity for injection type
     */
    private function getSeverityForInjectionType(string $type): string
    {
        return match ($type) {
            'sql_injection', 'command_injection' => 'critical',
            'xss', 'path_traversal' => 'high',
            default => 'medium'
        };
    }

    /**
     * Get maximum severity from findings
     */
    private function getMaxSeverityFromFindings(array $findings): DetectionSeverity
    {
        $maxSeverity = DetectionSeverity::INFO;

        foreach ($findings as $finding) {
            $severity = match ($finding['severity']) {
                'critical' => DetectionSeverity::CRITICAL,
                'high' => DetectionSeverity::HIGH,
                'medium' => DetectionSeverity::MEDIUM,
                'low' => DetectionSeverity::LOW,
                default => DetectionSeverity::INFO
            };

            if ($severity->getCvssScore() > $maxSeverity->getCvssScore()) {
                $maxSeverity = $severity;
            }
        }

        return $maxSeverity;
    }

    public function getConfiguration(): array
    {
        return [
            'enabled' => $this->enabled,
            'max_cookie_count' => $this->maxCookieCount,
            'max_cookie_size' => $this->maxCookieSize,
            'max_cookie_name_length' => $this->maxCookieNameLength,
            'max_total_cookie_size' => $this->maxTotalCookieSize,
            'suspicious_cookie_names' => $this->suspiciousCookieNames,
            'required_security_attributes' => $this->requiredSecurityAttributes,
            'injection_categories' => array_keys($this->injectionPatterns),
            'total_patterns' => array_sum(array_map('count', $this->injectionPatterns)),
        ];
    }

    public function isEnabled(): bool
    {
        return $this->enabled;
    }

    public function getPriority(): int
    {
        return $this->getType()->getPriority();
    }

    public function getExpectedProcessingTime(): int
    {
        return $this->getType()->getMaxProcessingTime();
    }

    public function supportsParallelExecution(): bool
    {
        return $this->getType()->canRunInParallel();
    }

    public function getDependencies(): array
    {
        return $this->getType()->getDependencies();
    }
}
