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

/**
 * JSON Payload Structure Analyzer
 * Analyzes JSON payloads for security threats and structural anomalies
 */
final class JsonAnalyzer implements AnalyzerInterface
{
    public function __construct(
        private readonly bool $enabled = true,
        private readonly int $maxJsonDepth = 32,
        private readonly int $maxJsonSize = 1048576, // 1MB
        private readonly int $maxArrayElements = 10000,
        private readonly int $maxObjectProperties = 1000,
        private readonly int $maxStringLength = 65536, // 64KB
        private readonly array $suspiciousPropertyNames = [
            'eval', 'exec', 'system', 'shell', 'command', 'cmd',
            'constructor', 'prototype', '__proto__', '__defineGetter__', '__defineSetter__',
            'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf',
            'script', 'javascript', 'vbscript', 'onload', 'onerror',
            'admin', 'root', 'password', 'pass', 'auth', 'token',
            'config', 'settings', 'debug', 'test', 'dev',
        ],
        private readonly array $injectionPatterns = [
            // JavaScript injection patterns
            'javascript_injection' => [
                '/(?i:function[\\s]*\\()/,',
                '/(?i:(?:eval|exec|setTimeout|setInterval)[\\s]*\\()/,',
                '/(?i:(?:alert|confirm|prompt)[\\s]*\\()/,',
                '/(?i:(?:document|window|navigator)[\\s]*\\.)/,',
            ],
            // NoSQL injection patterns
            'nosql_injection' => [
                '/(?i:\\$(?:where|ne|eq|gt|gte|lt|lte|in|nin|regex|exists|type|size|all|elemMatch))/',
                '/(?i:\\{\\s*\\$(?:where|ne|eq|gt|gte|lt|lte|in|nin|regex|exists|type|size|all|elemMatch))/',
                '/(?i:(?:this|obj|db)\\s*\\.)/',
            ],
            // SQL injection in JSON
            '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
            '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)/',
            ],
        ]
    ) {
    }

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

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

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

        $jsonBody = $requestData->body;
        $jsonSize = strlen($jsonBody);

        $metadata['json_size'] = $jsonSize;
        $metadata['content_type'] = $requestData->contentType;

        // Check JSON size limits
        if ($jsonSize > $this->maxJsonSize) {
            $findings[] = [
                'type' => 'oversized_json',
                'message' => "JSON payload too large: {$jsonSize} bytes (max: {$this->maxJsonSize})",
                'severity' => 'high',
                'size' => $jsonSize,
                'limit' => $this->maxJsonSize,
            ];
            $severity = DetectionSeverity::HIGH;
            $message = 'JSON payload exceeds size limits';
        }

        // Attempt to parse JSON
        $jsonData = null;
        $parseError = null;

        try {
            $jsonData = json_decode($jsonBody, true, $this->maxJsonDepth, JSON_THROW_ON_ERROR);
        } catch (\JsonException $e) {
            $parseError = $e->getMessage();
            $findings[] = [
                'type' => 'json_parse_error',
                'message' => "JSON parsing failed: {$parseError}",
                'severity' => 'medium',
                'error' => $parseError,
            ];
            $severity = DetectionSeverity::MEDIUM;
        }

        if ($jsonData !== null) {
            // Analyze JSON structure
            $structureAnalysis = $this->analyzeJsonStructure($jsonData);
            $metadata = array_merge($metadata, $structureAnalysis['metadata']);
            if (! empty($structureAnalysis['findings'])) {
                $findings = array_merge($findings, $structureAnalysis['findings']);
                $structureSeverity = $this->getMaxSeverityFromFindings($structureAnalysis['findings']);
                if ($structureSeverity->getCvssScore() > $severity->getCvssScore()) {
                    $severity = $structureSeverity;
                }
            }

            // Check for injection patterns in JSON content
            $injectionResults = $this->checkForInjections($jsonData);
            if (! empty($injectionResults)) {
                $findings = array_merge($findings, $injectionResults);
                $injectionSeverity = $this->getMaxSeverityFromFindings($injectionResults);
                if ($injectionSeverity->getCvssScore() > $severity->getCvssScore()) {
                    $severity = $injectionSeverity;
                }
            }

            // Check for suspicious property names
            $propertyResults = $this->analyzeSuspiciousProperties($jsonData);
            if (! empty($propertyResults)) {
                $findings = array_merge($findings, $propertyResults);
            }

            // Check for prototype pollution attempts
            $pollutionResults = $this->checkPrototypePollution($jsonData);
            if (! empty($pollutionResults)) {
                $findings = array_merge($findings, $pollutionResults);
                $pollutionSeverity = $this->getMaxSeverityFromFindings($pollutionResults);
                if ($pollutionSeverity->getCvssScore() > $severity->getCvssScore()) {
                    $severity = $pollutionSeverity;
                }
            }
        }

        // Check for JSON bombing patterns in raw string
        $bombingResults = $this->checkJsonBombing($jsonBody);
        if (! empty($bombingResults)) {
            $findings = array_merge($findings, $bombingResults);
        }

        // Check for encoding attacks
        $encodingResults = $this->analyzeJsonEncoding($jsonBody);
        if (! empty($encodingResults)) {
            $findings = array_merge($findings, $encodingResults);
        }

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

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

        $metadata['analysis_duration_ms'] = $processingTime->toMilliseconds();
        $metadata['analyzer'] = 'JsonAnalyzer';
        $metadata['parse_successful'] = $jsonData !== null;

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

    /**
     * Analyze JSON structure for anomalies
     */
    private function analyzeJsonStructure($jsonData): array
    {
        $findings = [];
        $metadata = [];

        $analysis = $this->analyzeJsonElement($jsonData, '', 0);

        $metadata['max_depth'] = $analysis['max_depth'];
        $metadata['total_elements'] = $analysis['total_elements'];
        $metadata['total_arrays'] = $analysis['total_arrays'];
        $metadata['total_objects'] = $analysis['total_objects'];
        $metadata['total_strings'] = $analysis['total_strings'];
        $metadata['max_array_size'] = $analysis['max_array_size'];
        $metadata['max_object_size'] = $analysis['max_object_size'];
        $metadata['max_string_length'] = $analysis['max_string_length'];

        // Check depth limits
        if ($analysis['max_depth'] > $this->maxJsonDepth) {
            $findings[] = [
                'type' => 'excessive_json_depth',
                'message' => "JSON nesting too deep: {$analysis['max_depth']} levels (max: {$this->maxJsonDepth})",
                'severity' => 'high',
                'depth' => $analysis['max_depth'],
                'limit' => $this->maxJsonDepth,
            ];
        }

        // Check array size limits
        if ($analysis['max_array_size'] > $this->maxArrayElements) {
            $findings[] = [
                'type' => 'oversized_json_array',
                'message' => "JSON array too large: {$analysis['max_array_size']} elements (max: {$this->maxArrayElements})",
                'severity' => 'medium',
                'size' => $analysis['max_array_size'],
                'limit' => $this->maxArrayElements,
            ];
        }

        // Check object property limits
        if ($analysis['max_object_size'] > $this->maxObjectProperties) {
            $findings[] = [
                'type' => 'oversized_json_object',
                'message' => "JSON object too large: {$analysis['max_object_size']} properties (max: {$this->maxObjectProperties})",
                'severity' => 'medium',
                'size' => $analysis['max_object_size'],
                'limit' => $this->maxObjectProperties,
            ];
        }

        // Check string length limits
        if ($analysis['max_string_length'] > $this->maxStringLength) {
            $findings[] = [
                'type' => 'oversized_json_string',
                'message' => "JSON string too long: {$analysis['max_string_length']} characters (max: {$this->maxStringLength})",
                'severity' => 'medium',
                'length' => $analysis['max_string_length'],
                'limit' => $this->maxStringLength,
            ];
        }

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

    /**
     * Recursively analyze JSON elements
     */
    private function analyzeJsonElement($element, string $path, int $depth): array
    {
        $analysis = [
            'max_depth' => $depth,
            'total_elements' => 1,
            'total_arrays' => 0,
            'total_objects' => 0,
            'total_strings' => 0,
            'max_array_size' => 0,
            'max_object_size' => 0,
            'max_string_length' => 0,
        ];

        if (is_array($element)) {
            $analysis['total_arrays'] = 1;
            $analysis['max_array_size'] = count($element);

            foreach ($element as $index => $value) {
                $childPath = $path . '[' . $index . ']';
                $childAnalysis = $this->analyzeJsonElement($value, $childPath, $depth + 1);

                $analysis['max_depth'] = max($analysis['max_depth'], $childAnalysis['max_depth']);
                $analysis['total_elements'] += $childAnalysis['total_elements'];
                $analysis['total_arrays'] += $childAnalysis['total_arrays'];
                $analysis['total_objects'] += $childAnalysis['total_objects'];
                $analysis['total_strings'] += $childAnalysis['total_strings'];
                $analysis['max_array_size'] = max($analysis['max_array_size'], $childAnalysis['max_array_size']);
                $analysis['max_object_size'] = max($analysis['max_object_size'], $childAnalysis['max_object_size']);
                $analysis['max_string_length'] = max($analysis['max_string_length'], $childAnalysis['max_string_length']);
            }
        } elseif (is_object($element) || (is_array($element) && $this->isAssociativeArray($element))) {
            $analysis['total_objects'] = 1;
            $properties = is_object($element) ? get_object_vars($element) : $element;
            $analysis['max_object_size'] = count($properties);

            foreach ($properties as $key => $value) {
                $childPath = $path . '.' . $key;
                $childAnalysis = $this->analyzeJsonElement($value, $childPath, $depth + 1);

                $analysis['max_depth'] = max($analysis['max_depth'], $childAnalysis['max_depth']);
                $analysis['total_elements'] += $childAnalysis['total_elements'];
                $analysis['total_arrays'] += $childAnalysis['total_arrays'];
                $analysis['total_objects'] += $childAnalysis['total_objects'];
                $analysis['total_strings'] += $childAnalysis['total_strings'];
                $analysis['max_array_size'] = max($analysis['max_array_size'], $childAnalysis['max_array_size']);
                $analysis['max_object_size'] = max($analysis['max_object_size'], $childAnalysis['max_object_size']);
                $analysis['max_string_length'] = max($analysis['max_string_length'], $childAnalysis['max_string_length']);
            }
        } elseif (is_string($element)) {
            $analysis['total_strings'] = 1;
            $analysis['max_string_length'] = strlen($element);
        }

        return $analysis;
    }

    /**
     * Check if array is associative (object-like)
     */
    private function isAssociativeArray(array $array): bool
    {
        if (empty($array)) {
            return false;
        }

        return array_keys($array) !== range(0, count($array) - 1);
    }

    /**
     * Check for injection patterns in JSON data
     */
    private function checkForInjections($jsonData): array
    {
        $findings = [];
        $jsonString = json_encode($jsonData);

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

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

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

        return $findings;
    }

    /**
     * Analyze suspicious property names
     */
    private function analyzeSuspiciousProperties($jsonData): array
    {
        $findings = [];

        $this->traverseJsonForProperties($jsonData, '', function ($path, $key, $value) use (&$findings) {
            if (in_array(strtolower($key), $this->suspiciousPropertyNames, true)) {
                $findings[] = [
                    'type' => 'suspicious_json_property',
                    'message' => "Suspicious property name: '{$key}' at {$path}",
                    'severity' => 'medium',
                    'property' => $key,
                    'path' => $path,
                    'value' => is_string($value) ? substr($value, 0, 100) : gettype($value),
                ];
            }
        });

        return $findings;
    }

    /**
     * Check for prototype pollution attempts
     */
    private function checkPrototypePollution($jsonData): array
    {
        $findings = [];

        $pollutionPatterns = [
            '__proto__', 'constructor', 'prototype',
            '__defineGetter__', '__defineSetter__', '__lookupGetter__', '__lookupSetter__',
        ];

        $this->traverseJsonForProperties($jsonData, '', function ($path, $key, $value) use (&$findings, $pollutionPatterns) {
            if (in_array($key, $pollutionPatterns, true)) {
                $findings[] = [
                    'type' => 'prototype_pollution',
                    'message' => "Prototype pollution attempt: '{$key}' at {$path}",
                    'severity' => 'critical',
                    'property' => $key,
                    'path' => $path,
                    'attack_type' => 'JavaScript prototype pollution',
                ];
            }
        });

        return $findings;
    }

    /**
     * Traverse JSON structure and call callback for each property
     */
    private function traverseJsonForProperties($data, string $path, callable $callback): void
    {
        if (is_array($data)) {
            if ($this->isAssociativeArray($data)) {
                // Treat as object
                foreach ($data as $key => $value) {
                    $currentPath = $path ? "{$path}.{$key}" : $key;
                    $callback($currentPath, $key, $value);

                    if (is_array($value) || is_object($value)) {
                        $this->traverseJsonForProperties($value, $currentPath, $callback);
                    }
                }
            } else {
                // Treat as array
                foreach ($data as $index => $value) {
                    $currentPath = $path ? "{$path}[{$index}]" : "[{$index}]";

                    if (is_array($value) || is_object($value)) {
                        $this->traverseJsonForProperties($value, $currentPath, $callback);
                    }
                }
            }
        } elseif (is_object($data)) {
            foreach (get_object_vars($data) as $key => $value) {
                $currentPath = $path ? "{$path}.{$key}" : $key;
                $callback($currentPath, $key, $value);

                if (is_array($value) || is_object($value)) {
                    $this->traverseJsonForProperties($value, $currentPath, $callback);
                }
            }
        }
    }

    /**
     * Check for JSON bombing patterns
     */
    private function checkJsonBombing(string $jsonBody): array
    {
        $findings = [];

        // Check for excessive repetition
        $repetitionPattern = '/(.{10,50})(?:\1){10,}/';
        if (preg_match($repetitionPattern, $jsonBody, $matches)) {
            $findings[] = [
                'type' => 'json_bombing_repetition',
                'message' => 'Excessive repetition detected (potential JSON bomb)',
                'severity' => 'high',
                'pattern' => substr($matches[1], 0, 20) . '...',
                'repetitions' => substr_count($jsonBody, $matches[1]),
            ];
        }

        // Check for extremely nested structures in raw JSON
        $nestingLevel = 0;
        $maxNesting = 0;
        $length = strlen($jsonBody);

        for ($i = 0; $i < $length; $i++) {
            $char = $jsonBody[$i];
            if ($char === '{' || $char === '[') {
                $nestingLevel++;
                $maxNesting = max($maxNesting, $nestingLevel);
            } elseif ($char === '}' || $char === ']') {
                $nestingLevel--;
            }
        }

        if ($maxNesting > 100) {
            $findings[] = [
                'type' => 'json_bombing_nesting',
                'message' => "Extremely deep nesting detected: {$maxNesting} levels",
                'severity' => 'high',
                'nesting_level' => $maxNesting,
            ];
        }

        return $findings;
    }

    /**
     * Analyze JSON encoding for attacks
     */
    private function analyzeJsonEncoding(string $jsonBody): array
    {
        $findings = [];

        // Check for Unicode escapes
        $unicodeEscapes = preg_match_all('/\\\\u[0-9a-fA-F]{4}/', $jsonBody);
        if ($unicodeEscapes > 50) {
            $findings[] = [
                'type' => 'excessive_unicode_escapes',
                'message' => "Excessive Unicode escapes: {$unicodeEscapes} sequences",
                'severity' => 'medium',
                'count' => $unicodeEscapes,
            ];
        }

        // Check for potential bypass attempts using Unicode
        if (preg_match('/\\\\u00[0-9a-fA-F]{2}/', $jsonBody)) {
            $findings[] = [
                'type' => 'unicode_bypass_attempt',
                'message' => 'Potential Unicode-based bypass attempt',
                'severity' => 'medium',
            ];
        }

        return $findings;
    }

    /**
     * Get severity for injection type
     */
    private function getSeverityForInjectionType(string $type): string
    {
        return match ($type) {
            'javascript_injection', 'nosql_injection', 'sql_injection' => 'critical',
            'xss', 'command_injection' => '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_json_depth' => $this->maxJsonDepth,
            'max_json_size' => $this->maxJsonSize,
            'max_array_elements' => $this->maxArrayElements,
            'max_object_properties' => $this->maxObjectProperties,
            'max_string_length' => $this->maxStringLength,
            'suspicious_property_names' => $this->suspiciousPropertyNames,
            '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();
    }
}
