<?php

declare(strict_types=1);

namespace App\Shared\Infrastructure\RateLimit;

use function is_int;

use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * Service de rate limiting pour les tentatives de login.
 *
 * Stratégie de protection multi-niveaux :
 * - Délai progressif Fibonacci par email (1s, 1s, 2s, 3s, 5s, 8s, 13s, 21s, 34s, 55s, 89s max)
 * - CAPTCHA requis après 5 échecs sur le même email
 * - Blocage IP 15 min si trop de tentatives globales (20) ou échec CAPTCHA répété
 *
 * @see Story 1.4 - AC3: Protection contre brute force
 */
final readonly class LoginRateLimiter implements LoginRateLimiterInterface
{
    private const string EMAIL_ATTEMPTS_PREFIX = 'login_attempts:';
    private const string EMAIL_DELAY_PREFIX = 'login_delay:';
    private const string IP_ATTEMPTS_PREFIX = 'login_ip:';
    private const string IP_BLOCKED_PREFIX = 'login_ip_blocked:';
    private const int EMAIL_ATTEMPTS_TTL = 900; // 15 minutes
    private const int IP_ATTEMPTS_LIMIT = 20;

    public function __construct(
        private CacheItemPoolInterface $cache,
    ) {
    }

    public function check(Request $request, string $email): LoginRateLimitResult
    {
        $ip = $request->getClientIp() ?? 'unknown';

        // Vérifier si l'IP est bloquée
        if ($this->isIpBlocked($ip)) {
            $blockedItem = $this->cache->getItem($this->ipBlockedKey($ip));
            $blockedUntil = $blockedItem->get();
            $retryAfter = is_int($blockedUntil) ? max(0, $blockedUntil - time()) : 0;

            return LoginRateLimitResult::blocked($retryAfter);
        }

        // Vérifier si l'email est en période de délai (enforcement Fibonacci)
        $delayedUntil = $this->getDelayedUntil($email);
        if ($delayedUntil > time()) {
            $retryAfter = $delayedUntil - time();
            $attempts = $this->getAttempts($email);

            return LoginRateLimitResult::delayed($attempts, $retryAfter);
        }

        // Récupérer le nombre de tentatives pour cet email
        $attempts = $this->getAttempts($email);
        $delaySeconds = LoginRateLimitResult::fibonacciDelay($attempts);
        $requiresCaptcha = $attempts >= self::CAPTCHA_THRESHOLD;

        return LoginRateLimitResult::allowed($attempts, $delaySeconds, $requiresCaptcha);
    }

    public function recordFailure(Request $request, string $email): LoginRateLimitResult
    {
        $ip = $request->getClientIp() ?? 'unknown';

        // Incrémenter les tentatives pour l'email
        $emailAttempts = $this->incrementAttempts($email);

        // Incrémenter les tentatives pour l'IP
        $ipAttempts = $this->incrementIpAttempts($ip);

        // Bloquer l'IP si trop de tentatives globales
        if ($ipAttempts >= self::IP_ATTEMPTS_LIMIT) {
            $this->blockIp($ip);

            return LoginRateLimitResult::blocked(self::IP_BLOCK_DURATION);
        }

        $delaySeconds = LoginRateLimitResult::fibonacciDelay($emailAttempts);
        $requiresCaptcha = $emailAttempts >= self::CAPTCHA_THRESHOLD;

        // Enregistrer le timestamp de prochaine tentative autorisée
        // Cela permet d'enforcer le délai côté serveur
        if ($delaySeconds > 0) {
            $this->setDelayedUntil($email, time() + $delaySeconds);
        }

        return LoginRateLimitResult::allowed($emailAttempts, $delaySeconds, $requiresCaptcha);
    }

    public function reset(string $email): void
    {
        $key = self::EMAIL_ATTEMPTS_PREFIX . $this->normalizeEmail($email);
        $this->cache->deleteItem($key);
    }

    public function blockIp(string $ip): void
    {
        $item = $this->cache->getItem($this->ipBlockedKey($ip));
        $item->set(time() + self::IP_BLOCK_DURATION);
        $item->expiresAfter(self::IP_BLOCK_DURATION);
        $this->cache->save($item);
    }

    public function isIpBlocked(string $ip): bool
    {
        $item = $this->cache->getItem($this->ipBlockedKey($ip));

        if (!$item->isHit()) {
            return false;
        }

        $blockedUntil = $item->get();

        return $blockedUntil > time();
    }

    private function getAttempts(string $email): int
    {
        $key = self::EMAIL_ATTEMPTS_PREFIX . $this->normalizeEmail($email);
        $item = $this->cache->getItem($key);

        if (!$item->isHit()) {
            return 0;
        }

        $cached = $item->get();

        return is_int($cached) ? $cached : 0;
    }

    private function incrementAttempts(string $email): int
    {
        $key = self::EMAIL_ATTEMPTS_PREFIX . $this->normalizeEmail($email);
        $item = $this->cache->getItem($key);

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

        $item->set($attempts);
        $item->expiresAfter(self::EMAIL_ATTEMPTS_TTL);
        $this->cache->save($item);

        return $attempts;
    }

    private function incrementIpAttempts(string $ip): int
    {
        $key = self::IP_ATTEMPTS_PREFIX . $this->hashIp($ip);
        $item = $this->cache->getItem($key);

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

        $item->set($attempts);
        $item->expiresAfter(self::EMAIL_ATTEMPTS_TTL);
        $this->cache->save($item);

        return $attempts;
    }

    private function ipBlockedKey(string $ip): string
    {
        return self::IP_BLOCKED_PREFIX . $this->hashIp($ip);
    }

    private function getDelayedUntil(string $email): int
    {
        $key = self::EMAIL_DELAY_PREFIX . $this->normalizeEmail($email);
        $item = $this->cache->getItem($key);

        if (!$item->isHit()) {
            return 0;
        }

        $cached = $item->get();

        return is_int($cached) ? $cached : 0;
    }

    private function setDelayedUntil(string $email, int $timestamp): void
    {
        $key = self::EMAIL_DELAY_PREFIX . $this->normalizeEmail($email);
        $item = $this->cache->getItem($key);
        $item->set($timestamp);
        // TTL = délai + marge de sécurité
        $item->expiresAfter(max(0, $timestamp - time()) + 10);
        $this->cache->save($item);
    }

    private function normalizeEmail(string $email): string
    {
        return strtolower(trim($email));
    }

    private function hashIp(string $ip): string
    {
        return hash('sha256', $ip);
    }
}
