<?php

declare(strict_types=1);

namespace App\Framework\Exception\Security;

use App\Framework\Exception\Core\SecurityErrorCode;
use App\Framework\Exception\ExceptionContext;
use App\Framework\Exception\FrameworkException;

/**
 * Ausnahme für SQL-Injection-Versuche
 *
 * Verwendet OWASP-konforme Nachrichten für SQL-Injection-Detection
 */
final class SqlInjectionAttemptException extends FrameworkException
{
    /**
     * @param string $clientIp Client-IP für Security-Tracking
     * @param string $field Betroffenes Feld/Parameter
     * @param string $pattern Erkanntes SQL-Injection-Pattern
     * @param string $originalValue Ursprünglicher Wert (sanitized)
     * @param string $detectionMethod Erkennungsmethode (regex, heuristic, etc.)
     * @param \Throwable|null $previous Vorherige Ausnahme
     */
    public function __construct(
        public readonly string $clientIp,
        public readonly string $field,
        public readonly string $pattern,
        public readonly string $originalValue,
        public readonly string $detectionMethod = 'pattern_match',
        ?\Throwable $previous = null
    ) {
        // OWASP-konforme Nachricht mit Platzhaltern
        $message = "Client {$this->clientIp} attempted SQL injection in field {$this->field}";

        $context = ExceptionContext::forOperation('security.sql_injection_detection', 'Security')
            ->withData([
                'client_ip' => $this->clientIp,
                'field' => $this->field,
                'pattern' => $this->pattern,
                'detection_method' => $this->detectionMethod,
                'value_length' => strlen($this->originalValue),
                'event_identifier' => "security_sql_injection:{$this->clientIp},{$this->field}",
                'category' => 'input_validation',
                'requires_alert' => true, // SQL-Injection-Versuche erfordern immer Alerts
            ])
            ->withDebug([
                'sanitized_value' => $this->sanitizeValueForLog($this->originalValue),
            ])
            ->withMetadata([
                'security_event' => true,
                'owasp_compliant' => true,
                'log_level' => 'ERROR',
                'attack_type' => 'sql_injection',
                'critical_security_event' => true,
            ]);

        parent::__construct(
            message: $message,
            context: $context,
            code: 400, // Bad Request
            previous: $previous,
            errorCode: SecurityErrorCode::SQL_INJECTION_DETECTED
        );
    }

    // === Factory Methods für verschiedene SQL-Injection-Patterns ===

    public static function unionSelect(string $clientIp, string $field, string $value): self
    {
        return new self($clientIp, $field, 'UNION SELECT', $value, 'union_detection');
    }

    public static function commentInjection(string $clientIp, string $field, string $value): self
    {
        return new self($clientIp, $field, 'SQL Comment (-- or /*)', $value, 'comment_detection');
    }

    public static function blindSqlInjection(string $clientIp, string $field, string $value): self
    {
        return new self($clientIp, $field, 'Blind SQL Injection', $value, 'blind_detection');
    }

    public static function timeBasedInjection(string $clientIp, string $field, string $value): self
    {
        return new self($clientIp, $field, 'Time-based SQL Injection', $value, 'time_based_detection');
    }

    public static function errorBasedInjection(string $clientIp, string $field, string $value): self
    {
        return new self($clientIp, $field, 'Error-based SQL Injection', $value, 'error_based_detection');
    }

    public static function booleanBasedInjection(string $clientIp, string $field, string $value): self
    {
        return new self($clientIp, $field, 'Boolean-based SQL Injection', $value, 'boolean_detection');
    }

    public static function stackedQueries(string $clientIp, string $field, string $value): self
    {
        return new self($clientIp, $field, 'Stacked Queries', $value, 'stacked_queries_detection');
    }

    /**
     * Gibt OWASP-konforme Event-Daten zurück
     */
    public function getSecurityEventData(): array
    {
        return [
            'event_identifier' => "security_sql_injection:{$this->clientIp},{$this->field}",
            'description' => "Client {$this->clientIp} attempted SQL injection in field {$this->field}",
            'category' => 'input_validation',
            'log_level' => 'ERROR',
            'requires_alert' => true,
            'client_ip' => $this->clientIp,
            'field' => $this->field,
            'pattern' => $this->pattern,
            'detection_method' => $this->detectionMethod,
            'attack_severity' => $this->getAttackSeverity(),
        ];
    }

    /**
     * Bestimmt Schweregrad des Angriffs
     */
    public function getAttackSeverity(): string
    {
        return match ($this->detectionMethod) {
            'union_detection' => 'HIGH',
            'stacked_queries_detection' => 'CRITICAL',
            'error_based_detection' => 'HIGH',
            'time_based_detection' => 'MEDIUM',
            'blind_detection' => 'MEDIUM',
            'boolean_detection' => 'MEDIUM',
            'comment_detection' => 'LOW',
            default => 'MEDIUM',
        };
    }

    /**
     * SQL-Injection-Versuche erfordern immer Alerts
     */
    public function requiresAlert(): bool
    {
        return true;
    }

    /**
     * Prüft ob es ein automatisierter Angriff ist
     */
    public function isAutomatedAttack(): bool
    {
        // Heuristiken für automatisierte Angriffe
        $automatedPatterns = [
            'sqlmap',
            'havij',
            'pangolin',
            'union select',
            'order by',
            'group by',
        ];

        $lowerValue = strtolower($this->originalValue);
        foreach ($automatedPatterns as $pattern) {
            if (str_contains($lowerValue, $pattern)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Analysiert das SQL-Injection-Pattern
     */
    public function analyzePattern(): array
    {
        $analysis = [
            'type' => 'unknown',
            'technique' => 'unknown',
            'payload_type' => 'unknown',
            'database_fingerprint' => 'unknown',
            'automation_detected' => $this->isAutomatedAttack(),
        ];

        $lowerValue = strtolower($this->originalValue);

        // Typ-Erkennung
        if (str_contains($lowerValue, 'union')) {
            $analysis['type'] = 'union_based';
        } elseif (str_contains($lowerValue, 'sleep(') || str_contains($lowerValue, 'waitfor')) {
            $analysis['type'] = 'time_based';
        } elseif (str_contains($lowerValue, '1=1') || str_contains($lowerValue, '1=0')) {
            $analysis['type'] = 'boolean_based';
        } elseif (str_contains($lowerValue, 'error') || str_contains($lowerValue, 'convert(')) {
            $analysis['type'] = 'error_based';
        }

        // Technik-Erkennung
        if (str_contains($lowerValue, '/*') || str_contains($lowerValue, '--')) {
            $analysis['technique'] = 'comment_bypassing';
        } elseif (str_contains($lowerValue, 'char(') || str_contains($lowerValue, 'ascii(')) {
            $analysis['technique'] = 'encoding_bypassing';
        } elseif (str_contains($lowerValue, ';')) {
            $analysis['technique'] = 'stacked_queries';
        }

        // Database-Fingerprinting
        if (str_contains($lowerValue, 'mysql') || str_contains($lowerValue, 'information_schema')) {
            $analysis['database_fingerprint'] = 'mysql';
        } elseif (str_contains($lowerValue, 'postgres') || str_contains($lowerValue, 'pg_')) {
            $analysis['database_fingerprint'] = 'postgresql';
        } elseif (str_contains($lowerValue, 'mssql') || str_contains($lowerValue, 'sys.')) {
            $analysis['database_fingerprint'] = 'mssql';
        } elseif (str_contains($lowerValue, 'oracle') || str_contains($lowerValue, 'dual')) {
            $analysis['database_fingerprint'] = 'oracle';
        }

        return $analysis;
    }

    /**
     * Gibt benutzerfreundliche Fehlermeldung zurück (ohne Details zu verraten)
     */
    public function getUserMessage(): string
    {
        return "Invalid input detected. Please check your data and try again.";
    }

    /**
     * Bereinigt Wert für sicheres Logging
     */
    private function sanitizeValueForLog(string $value): string
    {
        // Begrenzen auf 100 Zeichen und entferne gefährliche Zeichen
        $sanitized = substr($value, 0, 100);
        $sanitized = preg_replace('/[<>"\']/', '*', $sanitized);
        $sanitized = preg_replace('/\s+/', ' ', $sanitized);

        return trim($sanitized);
    }

    /**
     * Gibt Empfehlung für Verteidigung zurück
     */
    public function getDefenseRecommendation(): string
    {
        return match ($this->detectionMethod) {
            'union_detection' => 'Implement parameterized queries and input validation.',
            'stacked_queries_detection' => 'Disable multiple statements and use stored procedures.',
            'error_based_detection' => 'Implement proper error handling and logging.',
            'time_based_detection' => 'Add request timeouts and rate limiting.',
            'blind_detection' => 'Implement comprehensive input validation and WAF rules.',
            default => 'Use parameterized queries, input validation, and proper escaping.',
        };
    }

    /**
     * Generiert IOC (Indicator of Compromise) für Security-Teams
     */
    public function generateIOC(): array
    {
        return [
            'type' => 'sql_injection_attempt',
            'source_ip' => $this->clientIp,
            'target_field' => $this->field,
            'pattern' => $this->pattern,
            'detection_method' => $this->detectionMethod,
            'severity' => $this->getAttackSeverity(),
            'timestamp' => date('Y-m-d H:i:s'),
            'automated' => $this->isAutomatedAttack(),
            'analysis' => $this->analyzePattern(),
        ];
    }

    /**
     * Prüft ob sofortige Gegenmaßnahmen erforderlich sind
     */
    public function requiresImmediateAction(): bool
    {
        return in_array($this->getAttackSeverity(), ['HIGH', 'CRITICAL']) || $this->isAutomatedAttack();
    }
}
