<?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;
use Normalizer;

/**
 * URL Path and Structure Analyzer
 * Analyzes URL paths for security threats and suspicious patterns
 */
final class UrlAnalyzer implements AnalyzerInterface
{
    public function __construct(
        private readonly bool $enabled = true,
        private readonly int $maxUrlLength = 2048,
        private readonly int $maxPathLength = 1024,
        private readonly int $maxPathDepth = 20,
        private readonly int $maxQueryStringLength = 1024,
        private readonly array $suspiciousPathPatterns = [
            // Path traversal patterns
            'path_traversal' => [
                '/(?i:(?:\\.\\.[\\\\/])|(?:[\\\\/]\\.\\.)|(?:\\.\\.\\\\\\\\)|(?:\\\\\\\\\\.\\.))/',
                '/(?i:(?:%2e%2e%2f)|(?:%2e%2e\\\\\\\\)|(?:\\.\\.%2f)|(?:\\.\\.%5c)|(?:%2e%2e%5c))/',
                '/(?i:(?:%c0%ae%c0%ae%c0%af)|(?:%c1%9c%c1%9c%c1%af))/', // Unicode bypasses
            ],
            // File access patterns
            'file_access' => [
                '/(?i:(?:\\/etc\\/passwd|\\/etc\\/shadow|\\/etc\\/hosts|\\/proc\\/|\\/sys\\/))/',
                '/(?i:(?:c:[\\\\\\\\\\\\/]|\\\\\\\\\\\\\\\\|windows[\\\\\\\\\\\\/]system32))/',
                '/(?i:(?:\\.htaccess|\\.htpasswd|web\\.config|php\\.ini|httpd\\.conf))/',
            ],
            // Script execution patterns
            'script_execution' => [
                '/(?i:\\.(?:php|asp|aspx|jsp|py|pl|cgi|sh|bat|cmd|exe)(?:[\\?\\/]|$))/',
                '/(?i:(?:eval|exec|system|shell_exec|passthru|file_get_contents)\\s*\\()/',
            ],
            // SQL injection in URLs
            'sql_injection' => [
                '/(?i:union[\\s\\/\\*]+(?:all[\\s\\/\\*]+)?select)/',
                '/(?i:[\\\'\\"`][\\s]*(?:or|and)[\\s]*[\\\'\\"`]*[\\s]*(?:[\\\'\\"`]*[\\w]+[\\\'\\"`]*[\\s]*=[\\s]*[\\\'\\"`]*[\\w]+|[\\d]+[\\s]*=[\\s]*[\\d]+))/',
                '/(?i:(?:--|#|\\/\\*|\\*\\/))/',
                '/(?i:(?:information_schema|sys\\.tables|mysql\\.user|pg_tables))/',
            ],
            // XSS patterns in URLs
            '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)/',
            ],
            // Command injection
            'command_injection' => [
                '/(?i:(?:;|\\||\\|\\||&&|&|`|\\$\\(|\\${)[\\s]*(?:cat|ls|pwd|id|uname|whoami|ps|kill|rm|mv|cp|chmod|chown|find|grep|awk|sed|sort|head|tail|wc|netstat|ifconfig|ping|wget|curl|nc|telnet|ssh|su|sudo))/',
                '/(?i:(?:cmd|command)\\.exe|powershell|bash|sh|zsh|csh|tcsh|fish)[\\s]*(?:\\/c|\\/k|-c|-e)/',
            ],
        ],
        private readonly array $suspiciousFileExtensions = [
            'php', 'asp', 'aspx', 'jsp', 'py', 'pl', 'cgi', 'sh', 'bat', 'cmd', 'exe',
            'config', 'ini', 'conf', 'log', 'backup', 'bak', 'old', 'tmp', 'temp',
        ],
        private readonly array $adminPathIndicators = [
            'admin', 'administrator', 'management', 'manager', 'control', 'panel',
            'dashboard', 'backend', 'console', 'config', 'settings', 'setup',
        ],
        private readonly array $debugPathIndicators = [
            'debug', 'test', 'dev', 'development', 'staging', 'phpinfo',
            'info', 'status', 'health', 'metrics', 'stats',
        ]
    ) {
    }

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

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

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

        $url = $requestData->url;
        $path = $requestData->path;
        $queryString = $requestData->queryString;

        $metadata['url_length'] = strlen($url);
        $metadata['path_length'] = strlen($path);
        $metadata['query_string_length'] = strlen($queryString);

        // Check URL length limits
        if (strlen($url) > $this->maxUrlLength) {
            $findings[] = [
                'type' => 'oversized_url',
                'message' => "URL too long: " . strlen($url) . " chars (max: {$this->maxUrlLength})",
                'severity' => 'medium',
                'length' => strlen($url),
                'limit' => $this->maxUrlLength,
            ];
            $severity = DetectionSeverity::MEDIUM;
            $message = 'URL exceeds length limits';
        }

        // Check path length limits
        if (strlen($path) > $this->maxPathLength) {
            $findings[] = [
                'type' => 'oversized_path',
                'message' => "Path too long: " . strlen($path) . " chars (max: {$this->maxPathLength})",
                'severity' => 'medium',
                'length' => strlen($path),
                'limit' => $this->maxPathLength,
            ];
            if ($severity === DetectionSeverity::INFO || $severity === DetectionSeverity::LOW) {
                $severity = DetectionSeverity::MEDIUM;
            }
        }

        // Check query string length
        if (strlen($queryString) > $this->maxQueryStringLength) {
            $findings[] = [
                'type' => 'oversized_query_string',
                'message' => "Query string too long: " . strlen($queryString) . " chars (max: {$this->maxQueryStringLength})",
                'severity' => 'medium',
                'length' => strlen($queryString),
                'limit' => $this->maxQueryStringLength,
            ];
            if ($severity === DetectionSeverity::INFO || $severity === DetectionSeverity::LOW) {
                $severity = DetectionSeverity::MEDIUM;
            }
        }

        // Analyze path structure
        $pathAnalysis = $this->analyzePathStructure($path);
        $metadata = array_merge($metadata, $pathAnalysis['metadata']);
        if (! empty($pathAnalysis['findings'])) {
            $findings = array_merge($findings, $pathAnalysis['findings']);
            $pathSeverity = $this->getMaxSeverityFromFindings($pathAnalysis['findings']);
            if ($pathSeverity->getCvssScore() > $severity->getCvssScore()) {
                $severity = $pathSeverity;
            }
        }

        // Check for suspicious patterns in entire URL
        $patternResults = $this->checkSuspiciousPatterns($url);
        if (! empty($patternResults)) {
            $findings = array_merge($findings, $patternResults);
            $patternSeverity = $this->getMaxSeverityFromFindings($patternResults);
            if ($patternSeverity->getCvssScore() > $severity->getCvssScore()) {
                $severity = $patternSeverity;
            }
        }

        // Check for encoding evasion attempts
        $encodingResults = $this->analyzeEncoding($url);
        if (! empty($encodingResults)) {
            $findings = array_merge($findings, $encodingResults);
        }

        // Check for administrative path access
        if ($this->isAdminPath($path)) {
            $findings[] = [
                'type' => 'admin_path_access',
                'message' => "Access to administrative path: {$path}",
                'severity' => 'medium',
                'path' => $path,
                'reason' => 'Administrative interface access attempt',
            ];
            if ($severity === DetectionSeverity::INFO || $severity === DetectionSeverity::LOW) {
                $severity = DetectionSeverity::MEDIUM;
            }
        }

        // Check for debug/development path access
        if ($this->isDebugPath($path)) {
            $findings[] = [
                'type' => 'debug_path_access',
                'message' => "Access to debug/development path: {$path}",
                'severity' => 'low',
                'path' => $path,
                'reason' => 'Debug interface access attempt',
            ];
            if ($severity === DetectionSeverity::INFO) {
                $severity = DetectionSeverity::LOW;
            }
        }

        // Check for suspicious file extensions
        $fileExtensionResults = $this->analyzeFileExtension($path);
        if (! empty($fileExtensionResults)) {
            $findings = array_merge($findings, $fileExtensionResults);
        }

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

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

        // Check for Unicode normalization attacks
        $unicodeResults = $this->analyzeUnicodeThreats($url);
        if (! empty($unicodeResults)) {
            $findings = array_merge($findings, $unicodeResults);
        }

        $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 URL (" . count($criticalFindings) . " threats)";
            } elseif (! empty($highFindings)) {
                $message = count($highFindings) === 1
                    ? $highFindings[0]['message']
                    : "High-severity security issues detected in URL (" . count($highFindings) . " issues)";
            } else {
                $message = "Security issues detected in URL (" . count($findings) . " findings)";
            }
        }

        $metadata['analysis_duration_ms'] = $processingTime->toMilliseconds();
        $metadata['analyzer'] = 'UrlAnalyzer';

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

    /**
     * Analyze path structure and depth
     */
    private function analyzePathStructure(string $path): array
    {
        $findings = [];
        $metadata = [];

        // Split path into segments
        $segments = array_filter(explode('/', trim($path, '/')));
        $depth = count($segments);

        $metadata['path_depth'] = $depth;
        $metadata['path_segments'] = count($segments);

        // Check path depth
        if ($depth > $this->maxPathDepth) {
            $findings[] = [
                'type' => 'excessive_path_depth',
                'message' => "Path depth too deep: {$depth} levels (max: {$this->maxPathDepth})",
                'severity' => 'medium',
                'depth' => $depth,
                'limit' => $this->maxPathDepth,
            ];
        }

        // Analyze individual segments
        foreach ($segments as $index => $segment) {
            // Check for empty segments (double slashes)
            if (empty($segment)) {
                $findings[] = [
                    'type' => 'empty_path_segment',
                    'message' => "Empty path segment at position {$index}",
                    'severity' => 'low',
                    'position' => $index,
                ];
            }

            // Check for extremely long segments
            if (strlen($segment) > 255) {
                $findings[] = [
                    'type' => 'oversized_path_segment',
                    'message' => "Path segment too long: " . strlen($segment) . " chars",
                    'severity' => 'medium',
                    'segment' => substr($segment, 0, 50) . '...',
                    'length' => strlen($segment),
                    'position' => $index,
                ];
            }

            // Check for encoded segments
            if (preg_match('/%[0-9a-fA-F]{2}/', $segment)) {
                $metadata['encoded_segments'] = ($metadata['encoded_segments'] ?? 0) + 1;
            }

            // Check for suspicious characters in segments
            if (preg_match('/[<>"\']/', $segment)) {
                $findings[] = [
                    'type' => 'suspicious_path_characters',
                    'message' => "Suspicious characters in path segment: {$segment}",
                    'severity' => 'low',
                    'segment' => $segment,
                    'position' => $index,
                ];
            }
        }

        return [
            'findings' => $findings,
            'metadata' => $metadata,
        ];
    }

    /**
     * Check for suspicious patterns in URL
     */
    private function checkSuspiciousPatterns(string $url): array
    {
        $findings = [];

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

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

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

        return $findings;
    }

    /**
     * Analyze URL encoding and detect evasion attempts
     */
    private function analyzeEncoding(string $url): array
    {
        $findings = [];

        // Check for excessive URL encoding
        $urlEncodedChars = preg_match_all('/%[0-9a-fA-F]{2}/', $url);
        if ($urlEncodedChars > 20) {
            $findings[] = [
                'type' => 'excessive_url_encoding',
                'message' => "Excessive URL encoding: {$urlEncodedChars} encoded characters",
                'severity' => 'medium',
                'count' => $urlEncodedChars,
            ];
        }

        // Check for double encoding
        if (preg_match('/%25[0-9a-fA-F]{2}/', $url)) {
            $findings[] = [
                'type' => 'double_encoding',
                'message' => 'Double URL encoding detected (potential evasion)',
                'severity' => 'high',
            ];
        }

        // Check for Unicode evasion
        $unicodeChars = preg_match_all('/(?:%u[0-9a-fA-F]{4}|\\\\\\\\u[0-9a-fA-F]{4})/', $url);
        if ($unicodeChars > 0) {
            $findings[] = [
                'type' => 'unicode_evasion',
                'message' => "Unicode evasion attempt: {$unicodeChars} Unicode sequences",
                'severity' => 'medium',
                'count' => $unicodeChars,
            ];
        }

        // Check for alternative encoding schemes
        if (preg_match('/(?:%c0%ae|%c1%9c|%c0%2f|%c1%8s)/', $url)) {
            $findings[] = [
                'type' => 'alternative_encoding',
                'message' => 'Alternative encoding scheme detected (potential bypass)',
                'severity' => 'high',
            ];
        }

        return $findings;
    }

    /**
     * Check if path indicates administrative access
     */
    private function isAdminPath(string $path): bool
    {
        $normalizedPath = strtolower($path);

        foreach ($this->adminPathIndicators as $indicator) {
            if (str_contains($normalizedPath, $indicator)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Check if path indicates debug/development access
     */
    private function isDebugPath(string $path): bool
    {
        $normalizedPath = strtolower($path);

        foreach ($this->debugPathIndicators as $indicator) {
            if (str_contains($normalizedPath, $indicator)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Analyze file extension in path
     */
    private function analyzeFileExtension(string $path): array
    {
        $findings = [];

        // Extract file extension
        $pathInfo = pathinfo($path);
        $extension = strtolower($pathInfo['extension'] ?? '');

        if (! empty($extension)) {
            if (in_array($extension, $this->suspiciousFileExtensions, true)) {
                $severity = match ($extension) {
                    'php', 'asp', 'aspx', 'jsp', 'py', 'pl', 'cgi' => 'medium',
                    'sh', 'bat', 'cmd', 'exe' => 'high',
                    'config', 'ini', 'conf' => 'low',
                    default => 'low'
                };

                $findings[] = [
                    'type' => 'suspicious_file_extension',
                    'message' => "Access to file with suspicious extension: .{$extension}",
                    'severity' => $severity,
                    'extension' => $extension,
                    'path' => $path,
                ];
            }

            // Check for backup file indicators
            if (preg_match('/\.(backup|bak|old|tmp|temp|~)$/i', $path)) {
                $findings[] = [
                    'type' => 'backup_file_access',
                    'message' => "Access to potential backup file: {$path}",
                    'severity' => 'medium',
                    'path' => $path,
                ];
            }
        }

        return $findings;
    }

    /**
     * Analyze Unicode-based threats
     */
    private function analyzeUnicodeThreats(string $url): array
    {
        $findings = [];

        // Check for homograph attacks (similar-looking Unicode characters)
        if (preg_match('/[^\x00-\x7F]/', $url)) {
            $findings[] = [
                'type' => 'unicode_characters',
                'message' => 'Non-ASCII Unicode characters in URL (potential homograph attack)',
                'severity' => 'low',
            ];
        }

        // Check for Unicode normalization attacks (if Normalizer extension is available)
        if (class_exists('Normalizer')) {
            $normalized = Normalizer::normalize($url, Normalizer::FORM_C);
            if ($normalized !== false && strlen($url) !== strlen($normalized)) {
                $findings[] = [
                    'type' => 'unicode_normalization',
                    'message' => 'Unicode normalization discrepancy detected',
                    'severity' => 'medium',
                ];
            }
        }

        // Check for overlong UTF-8 sequences
        if (preg_match('/(?:%[cCdD][0-9a-fA-F])/', $url)) {
            $findings[] = [
                'type' => 'overlong_utf8',
                'message' => 'Potentially overlong UTF-8 sequence detected',
                'severity' => 'medium',
            ];
        }

        return $findings;
    }

    /**
     * Get severity level for attack category
     */
    private function getSeverityForCategory(string $category): string
    {
        return match ($category) {
            'sql_injection', 'command_injection', 'script_execution' => 'critical',
            'path_traversal', 'file_access', 'xss' => '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_url_length' => $this->maxUrlLength,
            'max_path_length' => $this->maxPathLength,
            'max_path_depth' => $this->maxPathDepth,
            'max_query_string_length' => $this->maxQueryStringLength,
            'suspicious_file_extensions' => $this->suspiciousFileExtensions,
            'admin_path_indicators' => $this->adminPathIndicators,
            'debug_path_indicators' => $this->debugPathIndicators,
            'pattern_categories' => array_keys($this->suspiciousPathPatterns),
            'total_patterns' => array_sum(array_map('count', $this->suspiciousPathPatterns)),
        ];
    }

    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();
    }
}
