<?php

declare(strict_types=1);

/*
 * This file is part of the VITICORE2 package.
 *
 * (c) CP Creation <web@cpcreation.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CpCreation\VitiCore\Commerce\Catalog\Outbox\Message;

use CpCreation\VitiCore\Commerce\Catalog\Configuration\CatalogWebhookEndpointResolver;
use CpCreation\VitiCore\Commerce\Catalog\Outbox\CatalogWebhookRequestFactory;
use CpCreation\VitiCore\Commerce\Catalog\Repository\CatalogChangeRepository;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException;
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

#[AsMessageHandler]
final readonly class DeliverCatalogChangeMessageHandler
{
    private const array RETRYABLE_STATUS_CODES = [408, 425, 429];

    public function __construct(
        private CatalogWebhookEndpointResolver $webhookEndpointResolver,
        private CatalogChangeRepository $repository,
        private CatalogWebhookRequestFactory $requestFactory,
        private HttpClientInterface $httpClient,
        private EntityManagerInterface $entityManager,
        private ?LoggerInterface $logger = null,
    ) {}

    public function __invoke(DeliverCatalogChangeMessage $message): void
    {
        $change = $this->repository->findOneByEventId($message->eventId);
        if (null === $change || $change->isDeliveryComplete() || !$this->webhookEndpointResolver->isReady()) {
            return;
        }

        $attemptedAt = new \DateTimeImmutable();
        $request = $this->requestFactory->create($change, $attemptedAt);

        try {
            $response = $this->httpClient->request('POST', $this->webhookEndpointResolver->url(), [
                'body' => $request->body,
                'headers' => $request->headers,
                'timeout' => 10,
                'max_duration' => 15,
                'max_redirects' => 0,
            ]);
            $statusCode = $response->getStatusCode();
        } catch (TransportExceptionInterface $exception) {
            $this->retry($change, $attemptedAt, 'Erreur réseau pendant la notification Commerce.', $exception);
        }

        if ($statusCode >= 200 && $statusCode < 300) {
            $change->markDelivered($attemptedAt);
            $this->entityManager->flush();

            return;
        }

        $error = \sprintf('Le webhook Commerce a répondu avec le statut HTTP %d.', $statusCode);
        if (\in_array($statusCode, self::RETRYABLE_STATUS_CODES, true) || $statusCode >= 500) {
            $this->retry($change, $attemptedAt, $error);
        }

        $change->markPermanentFailure($attemptedAt, $error);
        $this->entityManager->flush();
        $this->logger?->error($error, [
            'event_id' => $message->eventId,
            'status_code' => $statusCode,
        ]);

        throw new UnrecoverableMessageHandlingException($error);
    }

    private function retry(
        \CpCreation\VitiCore\Commerce\Catalog\Model\CatalogChange $change,
        \DateTimeImmutable $attemptedAt,
        string $error,
        ?\Throwable $previous = null,
    ): never {
        $change->markRetryableFailure($attemptedAt, $error);
        $this->entityManager->flush();
        $this->logger?->warning($error, [
            'event_id' => $change->getEventId()->toString(),
            'attempt' => $change->getDeliveryAttempts(),
        ]);

        throw new \RuntimeException($error, previous: $previous);
    }
}
