<?php

declare(strict_types=1);

namespace App\Framework\Exception\Http;

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

/**
 * Ausnahme für nicht gefundene Routen
 *
 * Verwendet OWASP-konforme Nachrichten für HTTP-Routing-Fehler
 */
final class RouteNotFoundException extends FrameworkException
{
    /**
     * @param string $path Angeforderte Route/Pfad
     * @param string $method HTTP-Methode
     * @param string $clientIp Client-IP für Security-Tracking
     * @param array $availableRoutes Verfügbare Routen (für Debugging)
     * @param \Throwable|null $previous Vorherige Ausnahme
     */
    public function __construct(
        public readonly string $path,
        public readonly string $method,
        public readonly string $clientIp = 'unknown',
        public readonly array $availableRoutes = [],
        ?\Throwable $previous = null
    ) {
        // OWASP-konforme Nachricht mit Platzhaltern
        $message = "Client {$this->clientIp} requested non-existent route {$this->method} {$this->path}";

        $context = ExceptionContext::forOperation('http.routing', 'Router')
            ->withData([
                'path' => $this->path,
                'method' => $this->method,
                'client_ip' => $this->clientIp,
                'available_routes_count' => count($this->availableRoutes),
                'event_identifier' => "http_route_not_found:{$this->clientIp},{$this->path}",
                'category' => 'http_routing',
                'requires_alert' => $this->requiresAlert(),
            ])
            ->withDebug([
                'available_routes' => $this->availableRoutes, // Nur im Debug-Modus
            ])
            ->withMetadata([
                'security_event' => true,
                'owasp_compliant' => true,
                'log_level' => 'INFO',
                'http_error' => true,
            ]);

        parent::__construct(
            message: $message,
            context: $context,
            code: 404, // Not Found
            previous: $previous,
            errorCode: HttpErrorCode::NOT_FOUND
        );
    }

    // === Factory Methods für verschiedene 404-Szenarien ===

    public static function path(string $path, string $method = 'GET', string $clientIp = 'unknown'): self
    {
        return new self($path, $method, $clientIp);
    }

    public static function api(string $apiPath, string $method, string $clientIp = 'unknown'): self
    {
        return new self("/api{$apiPath}", $method, $clientIp);
    }

    public static function admin(string $adminPath, string $method = 'GET', string $clientIp = 'unknown'): self
    {
        return new self("/admin{$adminPath}", $method, $clientIp);
    }

    public static function asset(string $assetPath, string $clientIp = 'unknown'): self
    {
        return new self($assetPath, 'GET', $clientIp);
    }

    /**
     * Gibt OWASP-konforme Event-Daten zurück
     */
    public function getSecurityEventData(): array
    {
        return [
            'event_identifier' => "http_route_not_found:{$this->clientIp},{$this->path}",
            'description' => "Client {$this->clientIp} requested non-existent route {$this->method} {$this->path}",
            'category' => 'http_routing',
            'log_level' => 'INFO',
            'requires_alert' => $this->requiresAlert(),
            'path' => $this->path,
            'method' => $this->method,
            'client_ip' => $this->clientIp,
        ];
    }

    /**
     * Prüft ob der 404-Fehler verdächtig ist (Scanning, Brute Force, etc.)
     */
    public function requiresAlert(): bool
    {
        // Alert bei verdächtigen Pfaden
        $suspiciousPaths = [
            '/admin', '/wp-admin', '/phpmyadmin', '/manager', '/console',
            '/.env', '/config', '/backup', '/test', '/debug',
            '/shell', '/cmd', '/execute', '/eval', '/system',
            '/../', '/..\\', '/etc/', '/var/', '/usr/', '/root/',
            '.php', '.asp', '.jsp', '.cgi', '.pl',
        ];

        foreach ($suspiciousPaths as $suspicious) {
            if (str_contains(strtolower($this->path), $suspicious)) {
                return true;
            }
        }

        // Alert bei ungewöhnlichen HTTP-Methoden auf normalen Pfaden
        if (in_array($this->method, ['TRACE', 'TRACK', 'CONNECT', 'OPTIONS']) && ! str_starts_with($this->path, '/api/')) {
            return true;
        }

        return false;
    }

    /**
     * Prüft ob es ein potentieller Scanner/Bot ist
     */
    public function isScannerActivity(): bool
    {
        $scannerPaths = [
            '/robots.txt', '/sitemap.xml', '/.well-known/',
            '/favicon.ico', '/apple-touch-icon', '/browserconfig.xml',
            '/crossdomain.xml', '/clientaccesspolicy.xml',
        ];

        foreach ($scannerPaths as $scanner) {
            if (str_starts_with($this->path, $scanner)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Schlägt ähnliche verfügbare Routen vor
     */
    public function getSimilarRoutes(): array
    {
        if (empty($this->availableRoutes)) {
            return [];
        }

        $similar = [];
        $requestedPath = strtolower($this->path);

        foreach ($this->availableRoutes as $route) {
            $routePath = strtolower($route);

            // Exakte Teilstring-Matches
            if (str_contains($routePath, $requestedPath) || str_contains($requestedPath, $routePath)) {
                $similar[] = $route;

                continue;
            }

            // Ähnlichkeit basierend auf Levenshtein-Distanz
            $distance = levenshtein($requestedPath, $routePath);
            if ($distance <= 3 && strlen($requestedPath) > 3) {
                $similar[] = $route;
            }
        }

        return array_slice($similar, 0, 5); // Maximal 5 Vorschläge
    }

    /**
     * Gibt benutzerfreundliche Fehlermeldung zurück
     */
    public function getUserMessage(): string
    {
        if ($this->isScannerActivity()) {
            return "The requested resource was not found.";
        }

        $similar = $this->getSimilarRoutes();
        if (! empty($similar)) {
            $suggestions = implode(', ', $similar);

            return "The requested page was not found. Did you mean: {$suggestions}?";
        }

        return "The requested page was not found. Please check the URL and try again.";
    }

    /**
     * Generiert sichere Log-Nachricht (ohne sensible Daten)
     */
    public function getLogMessage(): string
    {
        // IP-Adresse anonymisieren für Logs
        $anonymizedIp = $this->anonymizeIp($this->clientIp);

        return "Route not found: {$this->method} {$this->path} (Client: {$anonymizedIp})";
    }

    /**
     * Anonymisiert IP-Adresse für Logs
     */
    private function anonymizeIp(string $ip): string
    {
        if ($ip === 'unknown') {
            return $ip;
        }

        // IPv4
        if (str_contains($ip, '.')) {
            $parts = explode('.', $ip);
            if (count($parts) === 4) {
                return $parts[0] . '.' . $parts[1] . '.XXX.XXX';
            }
        }

        // IPv6
        if (str_contains($ip, ':')) {
            $parts = explode(':', $ip);
            if (count($parts) >= 3) {
                return $parts[0] . ':' . $parts[1] . ':XXXX::';
            }
        }

        return 'XXX.XXX.XXX.XXX';
    }
}
