<?php

declare(strict_types=1);

namespace App\Shared\Infrastructure\Messenger;

use InvalidArgumentException;
use LogicException;
use Override;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Retry\RetryStrategyInterface;
use Symfony\Component\Messenger\Stamp\RedeliveryStamp;
use Throwable;
use Twig\Error\LoaderError;

/**
 * Fibonacci-based retry strategy for Symfony Messenger.
 *
 * Delays: 1s, 1s, 2s, 3s, 5s, 8s, 13s (~33s total).
 * Transient errors (SMTP timeout, network) trigger retries.
 * Permanent errors (invalid template, bad argument) reject immediately.
 */
final readonly class FibonacciRetryStrategy implements RetryStrategyInterface
{
    private const int MAX_RETRIES = 7;

    /** @var list<int> Fibonacci delays in milliseconds */
    private const array FIBONACCI_DELAYS = [1000, 1000, 2000, 3000, 5000, 8000, 13000];

    /** @var list<class-string<Throwable>> */
    private const array PERMANENT_EXCEPTIONS = [
        InvalidArgumentException::class,
        LogicException::class,
        LoaderError::class,
    ];

    #[Override]
    public function isRetryable(Envelope $message, ?Throwable $throwable = null): bool
    {
        $retries = RedeliveryStamp::getRetryCountFromEnvelope($message);

        if ($retries >= self::MAX_RETRIES) {
            return false;
        }

        if ($throwable === null) {
            return true;
        }

        return !$this->isPermanentError($throwable);
    }

    #[Override]
    public function getWaitingTime(Envelope $message, ?Throwable $throwable = null): int
    {
        $retries = RedeliveryStamp::getRetryCountFromEnvelope($message);

        return self::FIBONACCI_DELAYS[$retries] ?? self::FIBONACCI_DELAYS[self::MAX_RETRIES - 1];
    }

    private function isPermanentError(Throwable $throwable): bool
    {
        foreach (self::PERMANENT_EXCEPTIONS as $permanentClass) {
            if ($throwable instanceof $permanentClass) {
                return true;
            }
        }

        $previous = $throwable->getPrevious();
        if ($previous !== null) {
            return $this->isPermanentError($previous);
        }

        return false;
    }
}
