<?php

declare(strict_types=1);

namespace App\Framework\Http\Middlewares;

use App\Framework\Http\Headers;
use App\Framework\Http\HttpMiddleware;
use App\Framework\Http\HttpResponse;
use App\Framework\Http\MiddlewareContext;
use App\Framework\Http\MiddlewarePriority;
use App\Framework\Http\MiddlewarePriorityAttribute;
use App\Framework\Http\Request;
use App\Framework\Http\Status;
use Predis\Client as RedisClient;

/**
 * Middleware zum Schutz vor zu vielen Anfragen von einer IP-Adresse.
 * Verwendet Redis zum Zählen der Anfragen und implementiert ein Sliding Window Limiting.
 */
#[MiddlewarePriorityAttribute(MiddlewarePriority::SECURITY)]
final class RateLimitingMiddleware implements XHttpMiddleware
{
    private const string REDIS_KEY_PREFIX = 'rate_limit:';

    /**
     * @param RedisClient $redis Redis-Client zum Speichern der Zähler
     * @param int $defaultLimit Standardmäßige Anzahl an erlaubten Anfragen im Zeitfenster
     * @param int $windowSeconds Länge des Zeitfensters in Sekunden
     * @param array<string, array{limit: int, window: int}> $pathLimits Spezifische Limits für bestimmte Pfade
     */
    public function __construct(
        private readonly RedisClient $redis,
        private readonly int $defaultLimit = 60,
        private readonly int $windowSeconds = 60,
        private readonly array $pathLimits = []
    ) {
    }

    public function __invoke(MiddlewareContext $context, callable $next): MiddlewareContext
    {
        $clientIp = $this->getClientIp($context->request);
        $path = $context->request->path;

        // Limits für den aktuellen Pfad ermitteln
        [$limit, $window] = $this->getLimitsForPath($path);

        // Redis-Schlüssel für diese IP und diesen Pfad
        $redisKey = $this->getRedisKey($clientIp, $path);

        // Anfrage zählen und prüfen
        $requestCount = $this->countRequest($redisKey, $window);

        // Rate-Limiting-Header zur Antwort hinzufügen
        $remaining = max(0, $limit - $requestCount);
        $context = $context->withResponseHeader('X-RateLimit-Limit', (string)$limit)
                           ->withResponseHeader('X-RateLimit-Remaining', (string)$remaining)
                           ->withResponseHeader('X-RateLimit-Reset', (string)$this->redis->ttl($redisKey));

        // Wenn das Limit überschritten wurde, 429-Antwort zurückgeben
        if ($requestCount > $limit) {
            $response = new HttpResponse(
                Status::TOO_MANY_REQUESTS,
                new Headers(['Content-Type' => 'application/json'])
            );

            // Retry-After Header hinzufügen
            $retryAfter = $this->redis->ttl($redisKey);
            $response = $response->withHeader('Retry-After', (string)$retryAfter);

            // JSON-Antwort mit Fehlermeldung
            $errorResponse = [
                'error' => 'Zu viele Anfragen',
                'message' => 'Sie haben das Limit für Anfragen überschritten. Bitte versuchen Sie es später erneut.',
                'retry_after' => $retryAfter
            ];

            $response->getBody()->write(json_encode($errorResponse));

            return $context->withResponse($response);
        }

        // Anfrage weiterleiten
        return $next($context);
    }

    /**
     * Zählt eine Anfrage und gibt die aktuelle Anzahl zurück.
     *
     * @param string $key Der Redis-Schlüssel für den Counter
     * @param int $window Das Zeitfenster in Sekunden
     * @return int Die aktuelle Anzahl an Anfragen im Zeitfenster
     */
    private function countRequest(string $key, int $window): int
    {
        // Aktuelle Zeit als Score (für Sorted Set)
        $now = time();

        // Alten Einträge entfernen (älter als das Zeitfenster)
        $this->redis->zremrangebyscore($key, 0, $now - $window);

        // Neue Anfrage hinzufügen
        $this->redis->zadd($key, [$now => $now]);

        // Gesamtanzahl der Anfragen im Zeitfenster ermitteln
        $count = $this->redis->zcard($key);

        // TTL für den Key setzen (Aufräumen)
        $this->redis->expire($key, $window);

        return (int)$count;
    }

    /**
     * Ermittelt die Limits für einen bestimmten Pfad.
     *
     * @param string $path Der Pfad
     * @return array{int, int} [limit, window]
     */
    private function getLimitsForPath(string $path): array
    {
        // Prüfen auf spezifische Pfad-Limits
        foreach ($this->pathLimits as $pattern => $config) {
            if (preg_match($pattern, $path)) {
                return [$config['limit'], $config['window']];
            }
        }

        // Standardwerte zurückgeben
        return [$this->defaultLimit, $this->windowSeconds];
    }

    /**
     * Generiert den Redis-Schlüssel für eine IP und einen Pfad.
     */
    private function getRedisKey(string $ip, string $path): string
    {
        // Für allgemeines Rate-Limiting alle Pfade gleich behandeln
        if (empty($this->pathLimits)) {
            return self::REDIS_KEY_PREFIX . $ip;
        }

        // Für pfad-spezifisches Rate-Limiting einen pfad-spezifischen Schlüssel erstellen
        $pathHash = md5($path);
        return self::REDIS_KEY_PREFIX . "{$ip}:{$pathHash}";
    }

    /**
     * Ermittelt die IP-Adresse des Clients unter Berücksichtigung von Proxies.
     */
    private function getClientIp(Request $request): string
    {
        $headers = [
            'X-Forwarded-For',
            'X-Real-IP',
            'HTTP_X_FORWARDED_FOR',
            'HTTP_CLIENT_IP',
            'REMOTE_ADDR'
        ];

        foreach ($headers as $header) {
            $value = $request->headers->get($header)[0] ?? null;
            if ($value) {
                // Bei mehreren IPs (X-Forwarded-For kann mehrere enthalten) die erste nehmen
                $ips = explode(',', $value);
                return trim($ips[0]);
            }
        }

        return '0.0.0.0';
    }
}
