<?php

declare(strict_types=1);

namespace App\Framework\Waf\Analysis\Analyzers;

use App\Framework\Core\ValueObjects\Byte;
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;

/**
 * File Upload Security Analyzer
 * Analyzes file uploads for security threats including malicious files, path traversal, and content validation
 */
final class FileAnalyzer implements AnalyzerInterface
{
    public function __construct(
        private readonly bool $enabled = true,
        private readonly int $maxFileSize = 10485760, // 10MB
        private readonly int $maxTotalFileSize = 52428800, // 50MB
        private readonly int $maxFileCount = 20,
        private readonly int $maxFilenameLength = 255,
        private readonly array $allowedMimeTypes = [
            'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml',
            'text/plain', 'text/csv', 'text/html', 'text/css', 'text/javascript',
            'application/pdf', 'application/json', 'application/xml',
            'application/zip', 'application/gzip',
            'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        ],
        private readonly array $dangerousExtensions = [
            'php', 'php3', 'php4', 'php5', 'phtml', 'pht', 'phps',
            'asp', 'aspx', 'ascx', 'ashx', 'asmx', 'cer', 'asa',
            'jsp', 'jspx', 'jsw', 'jsv', 'jspf',
            'py', 'pyc', 'pyo', 'pyw', 'pyz',
            'pl', 'pm', 'cgi',
            'rb', 'rbw',
            'sh', 'bash', 'zsh', 'csh', 'ksh', 'fish',
            'bat', 'cmd', 'com', 'pif', 'scr',
            'exe', 'msi', 'dll', 'scr', 'vbs', 'vbe', 'js', 'jse', 'ws', 'wsf', 'wsc', 'wsh',
            'ps1', 'ps1xml', 'ps2', 'ps2xml', 'psc1', 'psc2',
            'jar', 'class',
            'htaccess', 'htpasswd', 'ini', 'conf', 'config',
        ],
        private readonly array $suspiciousPatterns = [
            // Web shell patterns
            'web_shell' => [
                '/(?i:eval[\\s]*\\([\\s]*\\$_(?:GET|POST|REQUEST|COOKIE)\\[)/,',
                '/(?i:system[\\s]*\\([\\s]*\\$_(?:GET|POST|REQUEST|COOKIE)\\[)/,',
                '/(?i:exec[\\s]*\\([\\s]*\\$_(?:GET|POST|REQUEST|COOKIE)\\[)/,',
                '/(?i:shell_exec[\\s]*\\([\\s]*\\$_(?:GET|POST|REQUEST|COOKIE)\\[)/,',
                '/(?i:passthru[\\s]*\\([\\s]*\\$_(?:GET|POST|REQUEST|COOKIE)\\[)/,',
                '/(?i:file_get_contents[\\s]*\\([\\s]*\\$_(?:GET|POST|REQUEST|COOKIE)\\[)/,',
            ],
            // Malware signatures
            'malware' => [
                '/(?i:<%\\s*eval[\\s]*\\()/,',
                '/(?i:<script[^>]*>\\s*eval[\\s]*\\()/,',
                '/(?i:\\$[a-zA-Z_][a-zA-Z0-9_]*\\s*=\\s*["\'][^"\']*base64_decode)/,',
                '/(?i:gzinflate[\\s]*\\([\\s]*base64_decode)/,',
                '/(?i:str_rot13[\\s]*\\([\\s]*base64_decode)/,',
            ],
            // Path traversal
            'path_traversal' => [
                '/(?i:(?:\\.\\.[\\\\/])|(?:[\\\\/]\\.\\.)|(?:\\.\\.\\\\\\\\)|(?:\\\\\\\\\\.\\.))/',
                '/(?i:(?:%2e%2e%2f)|(?:%2e%2e\\\\\\\\)|(?:\\.\\.%2f)|(?:\\.\\.%5c)|(?:%2e%2e%5c))/',
            ],
            // Executable content
            'executable' => [
                '/^\\x7fELF/', // ELF header
                '/^MZ/', // PE header
                '/^\\xca\\xfe\\xba\\xbe/', // Java class file
                '/^\\xfe\\xed\\xfa/', // Mach-O header
            ],
        ]
    ) {
    }

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

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

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

        $files = $requestData->files;
        $fileCount = count($files);
        $totalFileSize = 0;

        $metadata['file_count'] = $fileCount;

        // Check file count limits
        if ($fileCount > $this->maxFileCount) {
            $findings[] = [
                'type' => 'excessive_file_count',
                'message' => "Too many files uploaded: {$fileCount} (max: {$this->maxFileCount})",
                'severity' => 'medium',
                'count' => $fileCount,
                'limit' => $this->maxFileCount,
            ];
            $severity = DetectionSeverity::MEDIUM;
            $message = 'Excessive number of files uploaded';
        }

        $dangerousFileCount = 0;
        $suspiciousFileCount = 0;
        $oversizedFileCount = 0;

        foreach ($files as $fieldName => $fileInfo) {
            if (! is_array($fileInfo)) {
                continue;
            }

            $fileName = $fileInfo['name'] ?? '';
            $fileSize = $fileInfo['size'] ?? 0;
            $fileTmpName = $fileInfo['tmp_name'] ?? '';
            $fileType = $fileInfo['type'] ?? '';
            $fileError = $fileInfo['error'] ?? UPLOAD_ERR_OK;

            $totalFileSize += $fileSize;

            // Analyze individual file
            $fileAnalysis = $this->analyzeIndividualFile(
                $fieldName,
                $fileName,
                $fileSize,
                $fileTmpName,
                $fileType,
                $fileError
            );

            if (! empty($fileAnalysis['findings'])) {
                $findings = array_merge($findings, $fileAnalysis['findings']);
                $fileSeverity = $this->getMaxSeverityFromFindings($fileAnalysis['findings']);
                if ($fileSeverity->getCvssScore() > $severity->getCvssScore()) {
                    $severity = $fileSeverity;
                }
            }

            // Count file types
            if ($fileAnalysis['is_dangerous']) {
                $dangerousFileCount++;
            }
            if ($fileAnalysis['is_suspicious']) {
                $suspiciousFileCount++;
            }
            if ($fileAnalysis['is_oversized']) {
                $oversizedFileCount++;
            }
        }

        // Check total file size
        if ($totalFileSize > $this->maxTotalFileSize) {
            $findings[] = [
                'type' => 'excessive_total_file_size',
                'message' => "Total file size too large: " . Byte::fromBytes($totalFileSize)->toHumanReadable() . " (max: " . Byte::fromBytes($this->maxTotalFileSize)->toHumanReadable() . ")",
                'severity' => 'high',
                'total_size' => $totalFileSize,
                'limit' => $this->maxTotalFileSize,
            ];
            $severity = DetectionSeverity::HIGH;
            $message = 'Total file size exceeds limits';
        }

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

        $metadata['total_file_size'] = $totalFileSize;
        $metadata['dangerous_file_count'] = $dangerousFileCount;
        $metadata['suspicious_file_count'] = $suspiciousFileCount;
        $metadata['oversized_file_count'] = $oversizedFileCount;
        $metadata['analysis_duration_ms'] = $processingTime->toMilliseconds();
        $metadata['analyzer'] = 'FileAnalyzer';

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

    /**
     * Analyze individual file upload
     */
    private function analyzeIndividualFile(
        string $fieldName,
        string $fileName,
        int $fileSize,
        string $fileTmpName,
        string $fileType,
        int $fileError
    ): array {
        $findings = [];
        $isDangerous = false;
        $isSuspicious = false;
        $isOversized = false;

        // Check upload errors
        if ($fileError !== UPLOAD_ERR_OK) {
            $errorMessage = $this->getUploadErrorMessage($fileError);
            $findings[] = [
                'type' => 'file_upload_error',
                'message' => "File upload error for '{$fileName}': {$errorMessage}",
                'severity' => 'medium',
                'field' => $fieldName,
                'filename' => $fileName,
                'error_code' => $fileError,
                'error_message' => $errorMessage,
            ];
        }

        // Check file size
        if ($fileSize > $this->maxFileSize) {
            $findings[] = [
                'type' => 'oversized_file',
                'message' => "File '{$fileName}' is too large: " . Byte::fromBytes($fileSize)->toHumanReadable(),
                'severity' => 'medium',
                'field' => $fieldName,
                'filename' => $fileName,
                'size' => $fileSize,
                'limit' => $this->maxFileSize,
            ];
            $isOversized = true;
        }

        // Check filename length
        if (strlen($fileName) > $this->maxFilenameLength) {
            $findings[] = [
                'type' => 'oversized_filename',
                'message' => "Filename too long: '{$fileName}' (" . strlen($fileName) . " chars)",
                'severity' => 'medium',
                'field' => $fieldName,
                'filename' => $fileName,
                'length' => strlen($fileName),
                'limit' => $this->maxFilenameLength,
            ];
        }

        // Check filename for path traversal
        if (preg_match('/(?:\\.\\.[\\\\/])|(?:[\\\\/]\\.\\.)|(?:\\.\\.\\\\\\\\)|(?:\\\\\\\\\\.\\.)|(?:%2e%2e%2f)|(?:%2e%2e\\\\\\\\)|(?:\\.\\.%2f)|(?:\\.\\.%5c)|(?:%2e%2e%5c)/', $fileName)) {
            $findings[] = [
                'type' => 'path_traversal_filename',
                'message' => "Path traversal attempt in filename: '{$fileName}'",
                'severity' => 'high',
                'field' => $fieldName,
                'filename' => $fileName,
            ];
            $isDangerous = true;
        }

        // Check for null bytes in filename
        if (str_contains($fileName, "\x00")) {
            $findings[] = [
                'type' => 'null_byte_filename',
                'message' => "Null byte in filename: '{$fileName}'",
                'severity' => 'high',
                'field' => $fieldName,
                'filename' => $fileName,
            ];
            $isDangerous = true;
        }

        // Check file extension
        $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
        if (! empty($extension) && in_array($extension, $this->dangerousExtensions, true)) {
            $severity = match ($extension) {
                'php', 'php3', 'php4', 'php5', 'phtml', 'asp', 'aspx', 'jsp' => 'critical',
                'exe', 'bat', 'cmd', 'sh', 'py', 'pl', 'rb' => 'high',
                default => 'medium'
            };

            $findings[] = [
                'type' => 'dangerous_file_extension',
                'message' => "Dangerous file extension: .{$extension} in '{$fileName}'",
                'severity' => $severity,
                'field' => $fieldName,
                'filename' => $fileName,
                'extension' => $extension,
            ];
            $isDangerous = true;
        }

        // Check MIME type
        if (! empty($fileType) && ! in_array($fileType, $this->allowedMimeTypes, true)) {
            $findings[] = [
                'type' => 'disallowed_mime_type',
                'message' => "Disallowed MIME type: {$fileType} for '{$fileName}'",
                'severity' => 'medium',
                'field' => $fieldName,
                'filename' => $fileName,
                'mime_type' => $fileType,
            ];
            $isSuspicious = true;
        }

        // Analyze file content if accessible
        if (! empty($fileTmpName) && is_file($fileTmpName) && is_readable($fileTmpName)) {
            $contentAnalysis = $this->analyzeFileContent($fileTmpName, $fileName, $fieldName);
            $findings = array_merge($findings, $contentAnalysis['findings']);

            if ($contentAnalysis['is_dangerous']) {
                $isDangerous = true;
            }
            if ($contentAnalysis['is_suspicious']) {
                $isSuspicious = true;
            }
        }

        // Check for double extensions (e.g., file.jpg.php)
        if (preg_match('/\\.([a-zA-Z0-9]+)\\.([a-zA-Z0-9]+)$/', $fileName, $matches)) {
            $firstExt = strtolower($matches[1]);
            $secondExt = strtolower($matches[2]);

            if (in_array($secondExt, $this->dangerousExtensions, true)) {
                $findings[] = [
                    'type' => 'double_extension',
                    'message' => "Double extension detected: .{$firstExt}.{$secondExt} in '{$fileName}'",
                    'severity' => 'high',
                    'field' => $fieldName,
                    'filename' => $fileName,
                    'first_extension' => $firstExt,
                    'second_extension' => $secondExt,
                ];
                $isDangerous = true;
            }
        }

        // Check for suspicious filename patterns
        $suspiciousPatterns = [
            '/(?i:shell|backdoor|webshell|c99|r57|b374k|wso|idx_file)/',
            '/(?i:eval|exec|system|cmd|command)/',
            '/(?i:config|passwd|shadow|htaccess|htpasswd)/',
        ];

        foreach ($suspiciousPatterns as $pattern) {
            if (preg_match($pattern, $fileName)) {
                $findings[] = [
                    'type' => 'suspicious_filename_pattern',
                    'message' => "Suspicious filename pattern: '{$fileName}'",
                    'severity' => 'medium',
                    'field' => $fieldName,
                    'filename' => $fileName,
                    'pattern' => $pattern,
                ];
                $isSuspicious = true;

                break;
            }
        }

        return [
            'findings' => $findings,
            'is_dangerous' => $isDangerous,
            'is_suspicious' => $isSuspicious,
            'is_oversized' => $isOversized,
        ];
    }

    /**
     * Analyze file content for threats
     */
    private function analyzeFileContent(string $filePath, string $fileName, string $fieldName): array
    {
        $findings = [];
        $isDangerous = false;
        $isSuspicious = false;

        try {
            // Read first few KB for analysis
            $handle = fopen($filePath, 'rb');
            if ($handle === false) {
                return ['findings' => [], 'is_dangerous' => false, 'is_suspicious' => false];
            }

            $content = fread($handle, 8192); // Read first 8KB
            fclose($handle);

            if ($content === false) {
                return ['findings' => [], 'is_dangerous' => false, 'is_suspicious' => false];
            }

            // Check for suspicious patterns
            foreach ($this->suspiciousPatterns as $category => $patterns) {
                foreach ($patterns as $pattern) {
                    if (preg_match($pattern, $content, $matches, PREG_OFFSET_CAPTURE)) {
                        $matchText = $matches[0][0];
                        $matchOffset = $matches[0][1];

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

                        $findings[] = [
                            'type' => $category . '_content',
                            'message' => "Suspicious {$category} content in '{$fileName}'",
                            'severity' => $severity,
                            'field' => $fieldName,
                            'filename' => $fileName,
                            'pattern' => $pattern,
                            'match' => substr($matchText, 0, 50), // Truncate for safety
                            'offset' => $matchOffset,
                            'category' => $category,
                        ];

                        if ($severity === 'critical' || $severity === 'high') {
                            $isDangerous = true;
                        } else {
                            $isSuspicious = true;
                        }
                    }
                }
            }

            // Check for embedded files (polyglot attacks)
            if ($this->detectEmbeddedFiles($content)) {
                $findings[] = [
                    'type' => 'embedded_file',
                    'message' => "Embedded file detected in '{$fileName}' (polyglot attack)",
                    'severity' => 'high',
                    'field' => $fieldName,
                    'filename' => $fileName,
                ];
                $isDangerous = true;
            }

            // Check MIME type vs actual content
            $detectedMimeType = $this->detectMimeType($content);
            $declaredMimeType = mime_content_type($filePath);

            if ($detectedMimeType && $declaredMimeType && $detectedMimeType !== $declaredMimeType) {
                $findings[] = [
                    'type' => 'mime_type_mismatch',
                    'message' => "MIME type mismatch in '{$fileName}': detected {$detectedMimeType}, declared {$declaredMimeType}",
                    'severity' => 'medium',
                    'field' => $fieldName,
                    'filename' => $fileName,
                    'detected_mime' => $detectedMimeType,
                    'declared_mime' => $declaredMimeType,
                ];
                $isSuspicious = true;
            }

        } catch (\Exception $e) {
            $findings[] = [
                'type' => 'content_analysis_error',
                'message' => "Error analyzing file content for '{$fileName}': " . $e->getMessage(),
                'severity' => 'low',
                'field' => $fieldName,
                'filename' => $fileName,
                'error' => $e->getMessage(),
            ];
        }

        return [
            'findings' => $findings,
            'is_dangerous' => $isDangerous,
            'is_suspicious' => $isSuspicious,
        ];
    }

    /**
     * Detect embedded files (polyglot attacks)
     */
    private function detectEmbeddedFiles(string $content): bool
    {
        $signatures = [
            'zip' => 'PK',
            'rar' => 'Rar!',
            'pdf' => '%PDF',
            'exe' => 'MZ',
            'elf' => "\x7fELF",
            'java' => "\xca\xfe\xba\xbe",
        ];

        $detectedCount = 0;
        foreach ($signatures as $type => $signature) {
            if (str_contains($content, $signature)) {
                $detectedCount++;
            }
        }

        // If more than one file signature is detected, it's likely a polyglot
        return $detectedCount > 1;
    }

    /**
     * Detect MIME type from content
     */
    private function detectMimeType(string $content): ?string
    {
        $signatures = [
            'image/jpeg' => ["\xff\xd8\xff"],
            'image/png' => ["\x89PNG\x0d\x0a\x1a\x0a"],
            'image/gif' => ["GIF87a", "GIF89a"],
            'application/pdf' => ["%PDF"],
            'application/zip' => ["PK\x03\x04", "PK\x05\x06", "PK\x07\x08"],
            'text/html' => ["<!DOCTYPE", "<html", "<HTML"],
            'text/xml' => ["<?xml"],
        ];

        foreach ($signatures as $mimeType => $sigs) {
            foreach ($sigs as $signature) {
                if (str_starts_with($content, $signature)) {
                    return $mimeType;
                }
            }
        }

        return null;
    }

    /**
     * Get upload error message
     */
    private function getUploadErrorMessage(int $errorCode): string
    {
        return match ($errorCode) {
            UPLOAD_ERR_INI_SIZE => 'File exceeds upload_max_filesize directive',
            UPLOAD_ERR_FORM_SIZE => 'File exceeds MAX_FILE_SIZE directive',
            UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
            UPLOAD_ERR_NO_FILE => 'No file was uploaded',
            UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
            UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
            UPLOAD_ERR_EXTENSION => 'File upload stopped by extension',
            default => "Unknown upload error (code: {$errorCode})"
        };
    }

    /**
     * Get severity for content type
     */
    private function getSeverityForContentType(string $type): string
    {
        return match ($type) {
            'web_shell', 'malware', 'executable' => 'critical',
            '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_file_size' => $this->maxFileSize,
            'max_total_file_size' => $this->maxTotalFileSize,
            'max_file_count' => $this->maxFileCount,
            'max_filename_length' => $this->maxFilenameLength,
            'allowed_mime_types' => $this->allowedMimeTypes,
            'dangerous_extensions' => $this->dangerousExtensions,
            'pattern_categories' => array_keys($this->suspiciousPatterns),
            'total_patterns' => array_sum(array_map('count', $this->suspiciousPatterns)),
        ];
    }

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