<?php

namespace App\MessageHandler;

use App\Entity\Domain;
use App\Entity\DomainPurchase;
use App\Entity\Watchlist;
use App\Message\OrderDomain;
use App\Notifier\DomainOrderErrorNotification;
use App\Notifier\DomainOrderNotification;
use App\Repository\DomainPurchaseRepository;
use App\Repository\DomainRepository;
use App\Repository\WatchlistRepository;
use App\Service\ChatNotificationService;
use App\Service\InfluxdbService;
use App\Service\Provider\AbstractProvider;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Mime\Address;
use Symfony\Component\Notifier\Recipient\Recipient;

#[AsMessageHandler]
final readonly class OrderDomainHandler
{
    private Address $sender;

    public function __construct(
        string $mailerSenderEmail,
        string $mailerSenderName,
        private WatchlistRepository $watchlistRepository,
        private DomainRepository $domainRepository,
        private KernelInterface $kernel,
        private MailerInterface $mailer,
        private LoggerInterface $logger,
        private ChatNotificationService $chatNotificationService,
        private InfluxdbService $influxdbService,
        #[Autowire(service: 'service_container')]
        private ContainerInterface $locator,
        #[Autowire(param: 'influxdb_enabled')]
        private bool $influxdbEnabled, private EntityManagerInterface $em,
        private DomainPurchaseRepository $domainPurchaseRepository,
        private LockFactory $lockFactory,
    ) {
        $this->sender = new Address($mailerSenderEmail, $mailerSenderName);
    }

    /**
     * @throws TransportExceptionInterface
     * @throws \Symfony\Component\Notifier\Exception\TransportExceptionInterface
     * @throws \Throwable
     */
    public function __invoke(OrderDomain $message): void
    {
        /** @var Watchlist $watchlist */
        $watchlist = $this->watchlistRepository->findOneBy(['token' => $message->watchlistToken]);
        /** @var Domain $domain */
        $domain = $this->domainRepository->findOneBy(['ldhName' => $message->ldhName]);

        $connector = $watchlist->getConnector();

        /*
         * We make sure that the domain name is marked absent from WHOIS in the database before continuing.
         * A connector must also be associated with the Watchlist to allow the purchase of the domain name.
         * Otherwise, we do nothing.
         */

        if (null === $connector || !$domain->getDeleted()) {
            return;
        }

        $lock = $this->lockFactory->createLockFromKey(
            new Key('domain_purchase.'.$domain->getLdhName().'.'.$connector->getId()),
            ttl: 600,
            autoRelease: false
        );

        if (!$lock->acquire()) {
            $this->logger->notice('Purchase attempt is already launched for this domain name with this connector', [
                'watchlist' => $message->watchlistToken,
                'connector' => $connector->getId(),
                'ldhName' => $message->ldhName,
                'provider' => $connector->getProvider()->value,
            ]);

            return;
        }

        $this->logger->notice('Watchlist is linked to a connector : a purchase attempt will be made for this domain name', [
            'watchlist' => $message->watchlistToken,
            'connector' => $connector->getId(),
            'ldhName' => $message->ldhName,
            'provider' => $connector->getProvider()->value,
        ]);

        try {
            $provider = $connector->getProvider();

            if (null === $provider) {
                throw new \InvalidArgumentException('Provider not found');
            }

            $connectorProviderClass = $provider->getConnectorProvider();
            /** @var AbstractProvider $connectorProvider */
            $connectorProvider = $this->locator->get($connectorProviderClass);

            /*
             * The user is authenticated to ensure that the credentials are still valid.
             * If no errors occur, the purchase is attempted.
             */
            $connectorProvider->authenticate($connector->getAuthData());

            $connectorProvider->orderDomain($domain, $this->kernel->isDebug());

            /*
             * If the purchase was successful, the statistics are updated and a success message is sent to the user.
             */
            $this->logger->notice('Watchlist is linked to connector : a purchase was successfully made for this domain name', [
                'watchlist' => $message->watchlistToken,
                'connector' => $connector->getId(),
                'ldhName' => $message->ldhName,
                'provider' => $connector->getProvider()->value,
            ]);

            if ($this->influxdbEnabled) {
                $this->influxdbService->addDomainOrderPoint($connector, $domain, true);
            }
            $notification = (new DomainOrderNotification($this->sender, $domain, $connector));
            $this->mailer->send($notification->asEmailMessage(new Recipient($watchlist->getUser()->getEmail()))->getMessage());
            $this->chatNotificationService->sendChatNotification($watchlist, $notification);

            $this->em->persist((new DomainPurchase())
                ->setDomain($domain)
                ->setConnector($connector)
                ->setConnectorProvider($connector->getProvider())
                ->setDomainOrderedAt(new \DateTimeImmutable())
                ->setUser($watchlist->getUser())
                ->setDomainDeletedAt($domain->getUpdatedAt())
                ->setDomainUpdatedAt($message->updatedAt));
            $this->em->flush();
        } catch (\Throwable $exception) {
            /*
             * The purchase was not successful (for several possible reasons that we have not determined).
             * The user is informed and the exception is raised, which may allow you to try again.
             */
            $this->logger->warning('Unable to complete purchase : an error message is sent to the user', [
                'watchlist' => $message->watchlistToken,
                'connector' => $connector->getId(),
                'ldhName' => $message->ldhName,
                'provider' => $connector->getProvider()->value,
            ]);

            if ($this->influxdbEnabled) {
                $this->influxdbService->addDomainOrderPoint($connector, $domain, false);
            }
            $notification = (new DomainOrderErrorNotification($this->sender, $domain));
            $this->mailer->send($notification->asEmailMessage(new Recipient($watchlist->getUser()->getEmail()))->getMessage());
            $this->chatNotificationService->sendChatNotification($watchlist, $notification);

            $domainPurchase = $this->domainPurchaseRepository->findOneBy([
                'domain' => $domain,
                'connector' => $connector,
                'domainOrderedAt' => null,
                'domainUpdatedAt' => $message->updatedAt,
                'user' => $watchlist->getUser(),
            ]);
            if (null === $domainPurchase) {
                $this->em->persist((new DomainPurchase())
                    ->setDomain($domain)
                    ->setConnector($connector)
                    ->setConnectorProvider($connector->getProvider())
                    ->setDomainOrderedAt(null)
                    ->setUser($watchlist->getUser())
                    ->setDomainDeletedAt($domain->getUpdatedAt())
                    ->setDomainUpdatedAt($message->updatedAt));
                $this->em->flush();
            }

            $lock->release();

            throw $exception;
        }
    }
}
