<?php

declare(strict_types=1);

namespace App\Framework\Retry\Strategies;

use App\Framework\Core\ValueObjects\Duration;
use App\Framework\Retry\RetryStrategy;
use Throwable;

/**
 * Exponential Backoff Retry-Strategie
 *
 * Verdoppelt die Wartezeit bei jedem Versuch: 100ms -> 200ms -> 400ms -> 800ms
 * Mit optionalem Jitter zur Vermeidung von Thundering Herd
 */
final readonly class ExponentialBackoffStrategy implements RetryStrategy
{
    private Duration $initialDelay;
    private Duration $maxDelay;

    public function __construct(
        private int $maxAttempts = 3,
        ?Duration $initialDelay = null,
        private float $multiplier = 2.0,
        ?Duration $maxDelay = null,
        private bool $useJitter = true,
        private array $retryableExceptions = [
            \RuntimeException::class,
            \Exception::class,
        ]
    ) {
        $this->initialDelay = $initialDelay ?? Duration::fromMilliseconds(100);
        $this->maxDelay = $maxDelay ?? Duration::fromSeconds(10);
    }

    public function shouldRetry(int $currentAttempt, Throwable $exception): bool
    {
        if ($currentAttempt >= $this->maxAttempts) {
            return false;
        }

        return $this->isRetryableException($exception);
    }

    public function getDelay(int $currentAttempt, Throwable $exception): Duration
    {
        if ($currentAttempt === 0) {
            return Duration::zero();
        }

        // Exponential: delay = initial * (multiplier ^ (attempt - 1))
        $delay = $this->initialDelay->toMilliseconds() *
                 pow($this->multiplier, $currentAttempt - 1);

        // Maximal-Grenze
        $delay = min($delay, $this->maxDelay->toMilliseconds());

        // Jitter hinzufügen (±25% Varianz)
        if ($this->useJitter) {
            $jitter = (int)($delay * 0.25);
            $delay = $delay + (random_int(-$jitter, $jitter));
        }

        return Duration::fromMilliseconds((int) $delay);
    }

    public function getMaxAttempts(): int
    {
        return $this->maxAttempts;
    }

    public function isRetryableException(Throwable $exception): bool
    {
        foreach ($this->retryableExceptions as $retryableClass) {
            if ($exception instanceof $retryableClass) {
                return true;
            }
        }

        return false;
    }

    /**
     * Factory Methods für häufige Szenarien
     */
    public static function forDatabase(int $maxAttempts = 3): self
    {
        return new self(
            maxAttempts: $maxAttempts,
            initialDelay: Duration::fromMilliseconds(50),
            multiplier: 2.0,
            maxDelay: Duration::fromSeconds(5),
            retryableExceptions: [
                \PDOException::class,
                'App\Framework\Database\Exception\DatabaseException',
            ]
        );
    }

    public static function forHttpClient(int $maxAttempts = 3): self
    {
        return new self(
            maxAttempts: $maxAttempts,
            initialDelay: Duration::fromMilliseconds(200),
            multiplier: 2.0,
            maxDelay: Duration::fromSeconds(10),
            retryableExceptions: [
                'App\Framework\HttpClient\Exception\HttpClientException',
                'App\Framework\HttpClient\Exception\CurlExecutionFailed',
            ]
        );
    }

    public static function forCache(int $maxAttempts = 2): self
    {
        return new self(
            maxAttempts: $maxAttempts,
            initialDelay: Duration::fromMilliseconds(10),
            multiplier: 3.0,
            maxDelay: Duration::fromMilliseconds(1000),
            useJitter: false, // Cache sollte schnell sein
            retryableExceptions: [
                \RedisException::class,
                \RuntimeException::class,
            ]
        );
    }
}
