<?php

declare(strict_types=1);

namespace App\Shared\Infrastructure\RateLimit;

use function sprintf;

/**
 * Résultat de la vérification du rate limit pour le login.
 *
 * Stratégie de protection :
 * - Délai progressif Fibonacci après chaque échec (1s, 1s, 2s, 3s, 5s, 8s, 13s...)
 * - CAPTCHA requis après 5 échecs
 * - Blocage IP après échec CAPTCHA répété
 */
final readonly class LoginRateLimitResult
{
    private function __construct(
        public bool $isAllowed,
        public int $attempts,
        public int $delaySeconds,
        public bool $requiresCaptcha,
        public bool $ipBlocked,
        public ?int $retryAfter,
    ) {
    }

    /**
     * Tentative autorisée (éventuellement avec délai).
     */
    public static function allowed(int $attempts, int $delaySeconds, bool $requiresCaptcha): self
    {
        return new self(
            isAllowed: true,
            attempts: $attempts,
            delaySeconds: $delaySeconds,
            requiresCaptcha: $requiresCaptcha,
            ipBlocked: false,
            retryAfter: $delaySeconds > 0 ? $delaySeconds : null,
        );
    }

    /**
     * IP bloquée (trop de tentatives ou échec CAPTCHA).
     */
    public static function blocked(int $retryAfter): self
    {
        return new self(
            isAllowed: false,
            attempts: 0,
            delaySeconds: $retryAfter,
            requiresCaptcha: false,
            ipBlocked: true,
            retryAfter: $retryAfter,
        );
    }

    /**
     * Tentative refusée temporairement (délai Fibonacci en cours).
     */
    public static function delayed(int $attempts, int $retryAfter): self
    {
        return new self(
            isAllowed: false,
            attempts: $attempts,
            delaySeconds: $retryAfter,
            requiresCaptcha: $attempts >= 5, // CAPTCHA_THRESHOLD
            ipBlocked: false,
            retryAfter: $retryAfter,
        );
    }

    /**
     * Calcule le délai Fibonacci pour un nombre de tentatives donné.
     *
     * Suite: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89... (max 89s)
     *
     * Mapping:
     * - 1 tentative = pas de délai
     * - 2 tentatives = 1s (F0)
     * - 3 tentatives = 1s (F1)
     * - 4 tentatives = 2s (F2)
     * - 5 tentatives = 3s (F3)
     * - etc.
     */
    public static function fibonacciDelay(int $attempts): int
    {
        if ($attempts <= 1) {
            return 0; // Première tentative sans délai
        }

        // Index dans la suite Fibonacci: attempts - 2
        // Cap à F(10) = 89 secondes (index 10 dans la suite 1,1,2,3,5,8,13,21,34,55,89)
        $n = min($attempts - 2, 10);

        return self::fibonacci($n);
    }

    /**
     * Calcule le n-ième nombre de Fibonacci.
     *
     * F(0)=1, F(1)=1, F(2)=2, F(3)=3, F(4)=5, F(5)=8, F(6)=13, F(7)=21, F(8)=34, F(9)=55, F(10)=89
     */
    private static function fibonacci(int $n): int
    {
        if ($n <= 1) {
            return 1;
        }

        $prev = 1;
        $curr = 1;

        for ($i = 2; $i <= $n; ++$i) {
            $next = $prev + $curr;
            $prev = $curr;
            $curr = $next;
        }

        return $curr;
    }

    /**
     * Génère les headers pour la réponse HTTP.
     *
     * @return array<string, string>
     */
    public function toHeaders(): array
    {
        $headers = [
            'X-Login-Attempts' => (string) $this->attempts,
        ];

        if ($this->delaySeconds > 0) {
            $headers['X-Login-Delay'] = (string) $this->delaySeconds;
            $headers['Retry-After'] = (string) $this->delaySeconds;
        }

        if ($this->requiresCaptcha) {
            $headers['X-Captcha-Required'] = 'true';
        }

        if ($this->ipBlocked) {
            $headers['X-IP-Blocked'] = 'true';
        }

        return $headers;
    }

    /**
     * Retourne le temps d'attente formaté pour l'utilisateur.
     */
    public function getFormattedDelay(): string
    {
        if ($this->delaySeconds <= 0) {
            return '';
        }

        if ($this->delaySeconds < 60) {
            return sprintf('%d seconde%s', $this->delaySeconds, $this->delaySeconds > 1 ? 's' : '');
        }

        $minutes = (int) ceil($this->delaySeconds / 60);

        return sprintf('%d minute%s', $minutes, $minutes > 1 ? 's' : '');
    }
}
