<?php

declare(strict_types=1);

namespace App\Shared\Infrastructure\RateLimit;

use App\Shared\Infrastructure\Captcha\TurnstileValidatorInterface;

use function is_array;
use function is_int;
use function is_string;

use Psr\Cache\CacheItemPoolInterface;

use function sprintf;

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
 * Vérifie le rate limit AVANT l'authentification.
 *
 * Ce listener intercepte les requêtes de login et :
 * - Bloque immédiatement si l'IP est bloquée
 * - Exige un CAPTCHA après 5 échecs et le valide via Cloudflare Turnstile
 * - Bloque l'IP si le CAPTCHA échoue 3 fois
 *
 * @see Story 1.4 - AC3: Protection contre brute force
 */
#[AsEventListener(event: KernelEvents::REQUEST, priority: 10)]
final readonly class LoginRateLimitListener
{
    private const int MAX_CAPTCHA_FAILURES = 3;
    private const int CAPTCHA_FAILURES_TTL = 900; // 15 minutes

    public function __construct(
        private LoginRateLimiterInterface $rateLimiter,
        private TurnstileValidatorInterface $turnstileValidator,
        private CacheItemPoolInterface $rateLimiterCache,
    ) {
    }

    public function __invoke(RequestEvent $event): void
    {
        $request = $event->getRequest();

        // Seulement pour la route de login
        if ($request->getPathInfo() !== '/api/login' || $request->getMethod() !== 'POST') {
            return;
        }

        // Extraire l'email du body JSON (avec guards contre JSON invalide)
        $content = json_decode($request->getContent(), true);

        if (!is_array($content)) {
            return; // JSON invalide, laisser le validator gérer
        }

        $email = isset($content['email']) && is_string($content['email']) ? $content['email'] : null;

        if ($email === null) {
            return; // Laisser le validator gérer
        }

        // Vérifier l'état du rate limit
        $result = $this->rateLimiter->check($request, $email);

        // IP bloquée → 429 immédiat
        if ($result->ipBlocked) {
            $event->setResponse($this->createBlockedResponse($result));

            return;
        }

        // Délai Fibonacci en cours (enforcement serveur) → 429
        if (!$result->isAllowed && $result->retryAfter !== null && $result->retryAfter > 0) {
            $event->setResponse($this->createDelayedResponse($result));

            return;
        }

        // CAPTCHA requis (après 5 échecs)
        if ($result->requiresCaptcha) {
            $captchaToken = isset($content['captcha_token']) && is_string($content['captcha_token'])
                ? $content['captcha_token']
                : null;

            // Pas de token fourni → demander le CAPTCHA
            if ($captchaToken === null || $captchaToken === '') {
                $event->setResponse($this->createCaptchaRequiredResponse($result));

                return;
            }

            // Valider le token via Cloudflare Turnstile
            $ip = $request->getClientIp();
            $turnstileResult = $this->turnstileValidator->validate($captchaToken, $ip);

            if (!$turnstileResult->isValid) {
                // CAPTCHA invalide → incrémenter les échecs CAPTCHA par IP
                // Après 3 échecs CAPTCHA, bloquer l'IP
                $captchaFailures = $this->recordCaptchaFailure($ip ?? 'unknown');

                if ($captchaFailures >= self::MAX_CAPTCHA_FAILURES) {
                    $this->rateLimiter->blockIp($ip ?? 'unknown');

                    $event->setResponse($this->createBlockedResponse(
                        LoginRateLimitResult::blocked(LoginRateLimiterInterface::IP_BLOCK_DURATION)
                    ));

                    return;
                }

                $event->setResponse($this->createCaptchaInvalidResponse(
                    $turnstileResult->errorMessage ?? 'Vérification échouée',
                    $captchaFailures,
                ));

                return;
            }

            // CAPTCHA valide → réinitialiser les échecs CAPTCHA pour cette IP
            $this->resetCaptchaFailures($ip ?? 'unknown');
        }

        // Tout est OK, continuer vers l'authentification
    }

    private function recordCaptchaFailure(string $ip): int
    {
        $key = 'captcha_failures_' . md5($ip);
        $item = $this->rateLimiterCache->getItem($key);

        $cached = $item->get();
        $failures = $item->isHit() && is_int($cached) ? $cached + 1 : 1;

        $item->set($failures);
        $item->expiresAfter(self::CAPTCHA_FAILURES_TTL);
        $this->rateLimiterCache->save($item);

        return $failures;
    }

    private function resetCaptchaFailures(string $ip): void
    {
        $key = 'captcha_failures_' . md5($ip);
        $this->rateLimiterCache->deleteItem($key);
    }

    private function createBlockedResponse(LoginRateLimitResult $result): JsonResponse
    {
        $response = new JsonResponse([
            'type' => '/errors/ip-blocked',
            'title' => 'Accès temporairement bloqué',
            'status' => Response::HTTP_TOO_MANY_REQUESTS,
            'detail' => sprintf(
                'Trop de tentatives de connexion. Réessayez dans %s.',
                $result->getFormattedDelay(),
            ),
            'retryAfter' => $result->retryAfter,
        ], Response::HTTP_TOO_MANY_REQUESTS);

        foreach ($result->toHeaders() as $name => $value) {
            $response->headers->set($name, $value);
        }

        return $response;
    }

    private function createDelayedResponse(LoginRateLimitResult $result): JsonResponse
    {
        $response = new JsonResponse([
            'type' => '/errors/rate-limited',
            'title' => 'Veuillez patienter',
            'status' => Response::HTTP_TOO_MANY_REQUESTS,
            'detail' => sprintf(
                'Veuillez patienter %s avant de réessayer.',
                $result->getFormattedDelay(),
            ),
            'retryAfter' => $result->retryAfter,
            'attempts' => $result->attempts,
        ], Response::HTTP_TOO_MANY_REQUESTS);

        foreach ($result->toHeaders() as $name => $value) {
            $response->headers->set($name, $value);
        }

        return $response;
    }

    private function createCaptchaRequiredResponse(LoginRateLimitResult $result): JsonResponse
    {
        $response = new JsonResponse([
            'type' => '/errors/captcha-required',
            'title' => 'Vérification requise',
            'status' => Response::HTTP_PRECONDITION_REQUIRED,
            'detail' => 'Veuillez compléter la vérification de sécurité pour continuer.',
            'attempts' => $result->attempts,
        ], Response::HTTP_PRECONDITION_REQUIRED);

        foreach ($result->toHeaders() as $name => $value) {
            $response->headers->set($name, $value);
        }

        return $response;
    }

    private function createCaptchaInvalidResponse(string $errorMessage, int $failures): JsonResponse
    {
        return new JsonResponse([
            'type' => '/errors/captcha-invalid',
            'title' => 'Vérification échouée',
            'status' => Response::HTTP_BAD_REQUEST,
            'detail' => $errorMessage,
            'captchaFailures' => $failures,
            'maxFailures' => self::MAX_CAPTCHA_FAILURES,
        ], Response::HTTP_BAD_REQUEST);
    }
}
