<?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\Marketing;

use CpCreation\VitiCore\Bridge\Symfony\Bundle\Mailer\Command\ReviewInvitationMailCommand;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Mailer\Command\ReviewInvitationReminderMailCommand;
use CpCreation\VitiCore\Cart\Repository\CartRepository;
use CpCreation\VitiCore\Marketing\Repository\ReviewInvitationRepository;
use CpCreation\VitiCore\Marketing\Repository\ReviewRequestConfigurationRepository;
use CpCreation\VitiCore\Marketing\Repository\ReviewSolicitationOptOutRepository;
use Symfony\Component\Messenger\MessageBusInterface;

final readonly class ReviewInvitationDispatcher
{
    private const string STALE_REMINDER_CLAIM_INTERVAL = 'PT1H';

    public function __construct(
        private ReviewRequestConfigurationRepository $configurationRepository,
        private CartRepository $cartRepository,
        private ReviewInvitationRepository $invitationRepository,
        private ReviewInvitationIssuer $invitationIssuer,
        private ReviewSolicitationOptOutRepository $optOutRepository,
        private MessageBusInterface $messageBus,
    ) {}

    public function dispatchDue(?\DateTimeImmutable $now = null, int $limit = 100): ReviewInvitationDispatchResult
    {
        $configuration = $this->configurationRepository->findOne();
        if (null === $configuration || !$configuration->isEnabled()) {
            return new ReviewInvitationDispatchResult(0, 0, 0);
        }

        $now ??= new \DateTimeImmutable();
        $fulfilledBefore = $now->modify(sprintf('-%d days', $configuration->getSendDelayDays()));
        $carts = $this->cartRepository->findEligibleForReviewInvitation(
            $fulfilledBefore,
            $configuration->getFulfilledAfter(),
            $limit,
        );

        $pendingMessages = [];
        $skippedCount = 0;

        foreach ($carts as $cart) {
            $recipientEmail = $cart->getEmail();
            if (null === $recipientEmail || false === filter_var($recipientEmail, \FILTER_VALIDATE_EMAIL)) {
                ++$skippedCount;

                continue;
            }
            if ($this->optOutRepository->hasOptedOut($recipientEmail)) {
                ++$skippedCount;

                continue;
            }

            $issued = $this->invitationIssuer->issue(
                cart: $cart,
                recipientEmail: $recipientEmail,
                tokenLifetimeDays: $configuration->getTokenLifetimeDays(),
                now: $now,
            );
            $pendingMessages[] = [$issued->invitation, $issued->token];
        }

        $this->invitationRepository->flush();

        foreach ($pendingMessages as [$invitation, $token]) {
            $this->messageBus->dispatch(new ReviewInvitationMailCommand(
                invitationId: $invitation->getId()->toString(),
                token: $token,
            ));
        }

        $reminderQueuedCount = $this->queueDueSecondReminders(
            $configuration->getSecondReminderDelayDays(),
            $configuration->isSecondReminderEnabled(),
            $now,
            $limit,
        );

        return new ReviewInvitationDispatchResult(
            eligibleCount: \count($carts),
            createdCount: \count($pendingMessages),
            skippedCount: $skippedCount,
            reminderQueuedCount: $reminderQueuedCount,
        );
    }

    private function queueDueSecondReminders(
        int $delayDays,
        bool $enabled,
        \DateTimeImmutable $now,
        int $limit,
    ): int {
        if (!$enabled) {
            return 0;
        }

        $invitations = $this->invitationRepository->findDueForSecondReminder(
            sentBefore: $now->modify(sprintf('-%d days', $delayDays)),
            now: $now,
            staleQueuedBefore: $now->sub(new \DateInterval(self::STALE_REMINDER_CLAIM_INTERVAL)),
            limit: $limit,
        );
        $pendingMessages = [];

        foreach ($invitations as $invitation) {
            if ($this->optOutRepository->hasOptedOut($invitation->getRecipientEmail())) {
                $invitation->suppressReminder($now);

                continue;
            }

            $token = self::createToken();
            if (!$invitation->scheduleReminder(hash('sha256', $token), $now)) {
                continue;
            }
            $pendingMessages[] = [$invitation, $token];
        }

        // Le claim est durable avant l’envoi dans Messenger. En cas de crash entre
        // les deux, il est repris après une heure avec un nouveau jeton.
        $this->invitationRepository->flush();

        foreach ($pendingMessages as [$invitation, $token]) {
            $this->messageBus->dispatch(new ReviewInvitationReminderMailCommand(
                invitationId: $invitation->getId()->toString(),
                token: $token,
            ));
        }

        return \count($pendingMessages);
    }

    private static function createToken(): string
    {
        return rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
    }
}
