<?php

declare(strict_types=1);

namespace CpCreation\VitiCore\Bridge\Symfony\Bundle\EventListener;

use CpCreation\VitiCore\Bridge\Symfony\Bundle\Mailer\EmailLogMetadata;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Mailer\EmailLogStamp;
use CpCreation\VitiCore\Cart\Model\Cart;
use CpCreation\VitiCore\Mailer\Model\EmailLog;
use CpCreation\VitiCore\Mailer\Repository\EmailLogRepository;
use CpCreation\VitiCore\User\Model\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Mailer\Event\FailedMessageEvent;
use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mailer\Event\SentMessageEvent;
use Symfony\Component\Mailer\Messenger\SendEmailMessage;
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email;
use Symfony\Component\Mime\RawMessage;

final class EmailLogSubscriber implements EventSubscriberInterface
{
    /** @var \WeakMap<RawMessage, string> */
    private \WeakMap $directEmailLogIds;

    public function __construct(
        private EmailLogRepository $emailLogs,
        private EntityManagerInterface $entityManager,
    ) {
        $this->directEmailLogIds = new \WeakMap();
    }

    public static function getSubscribedEvents(): array
    {
        return [
            MessageEvent::class => 'onMailerMessage',
            SentMessageEvent::class => 'onMailerMessageSent',
            FailedMessageEvent::class => 'onMailerMessageFailed',
            WorkerMessageHandledEvent::class => 'onWorkerMessageHandled',
            WorkerMessageFailedEvent::class => ['onWorkerMessageFailed', -200],
        ];
    }

    public function onMailerMessage(MessageEvent $event): void
    {
        $message = $event->getMessage();
        if (!$message instanceof Email) {
            return;
        }

        $directTransport = EmailLogMetadata::consumeDirectTransportMarker($message);

        // Les headers internes ne doivent pas sortir vers le serveur SMTP.
        // Sur le chemin async, le premier MessageEvent reçoit une copie : le
        // header reste donc sur le message original jusqu’au worker.
        $metadata = EmailLogMetadata::read($message, remove: true);
        if (null === $metadata) {
            return;
        }

        // Un email async possède déjà un EmailLogStamp créé avant sa mise en
        // file. Son MessageEvent non-queued dans le worker ne doit pas créer
        // un second journal. Seuls les envois directs explicitement marqués
        // sont corrélés aux événements SentMessage/FailedMessage.
        if (($event->isQueued() && $directTransport)
            || (!$event->isQueued() && !$directTransport)
        ) {
            return;
        }

        $recipient = $this->firstRecipient($message);
        if (null === $recipient) {
            return;
        }

        $log = EmailLog::queued(
            type: $metadata['type'],
            recipientEmail: $recipient->getAddress(),
            recipientName: '' !== $recipient->getName() ? $recipient->getName() : null,
            subject: $message->getSubject(),
            transport: EmailLog::labelForTransport($event->getTransport()),
            user: $this->findEntity(User::class, $metadata['userId'] ?? null),
            cart: $this->findEntity(Cart::class, $metadata['cartId'] ?? null),
        );

        $this->emailLogs->save($log);
        if ($event->isQueued()) {
            $event->addStamp(new EmailLogStamp($log->getId()->toString()));

            return;
        }

        $this->directEmailLogIds[$message] = $log->getId()->toString();
    }

    public function onMailerMessageSent(SentMessageEvent $event): void
    {
        $log = $this->resolveDirectLog($event->getMessage()->getOriginalMessage());
        if (null === $log) {
            return;
        }

        $log->markSent();
        $this->emailLogs->flush();
    }

    public function onMailerMessageFailed(FailedMessageEvent $event): void
    {
        $log = $this->resolveDirectLog($event->getMessage());
        if (null === $log) {
            return;
        }

        $log->markFailed($event->getError()->getMessage(), false);
        $this->emailLogs->flush();
    }

    public function onWorkerMessageHandled(WorkerMessageHandledEvent $event): void
    {
        if (!$this->isTrackedEmailMessage($event->getEnvelope()->getMessage())) {
            return;
        }

        $log = $this->resolveLog($event->getEnvelope()->last(EmailLogStamp::class));
        if (null === $log) {
            return;
        }

        $log->markSent();
        $this->emailLogs->flush();
    }

    public function onWorkerMessageFailed(WorkerMessageFailedEvent $event): void
    {
        if (!$this->isTrackedEmailMessage($event->getEnvelope()->getMessage())) {
            return;
        }

        $log = $this->resolveLog($event->getEnvelope()->last(EmailLogStamp::class));
        if (null === $log) {
            return;
        }

        $log->markFailed($event->getThrowable()->getMessage(), $event->willRetry());
        $this->emailLogs->flush();
    }

    private function firstRecipient(Email $email): ?Address
    {
        return $email->getTo()[0] ?? null;
    }

    /**
     * @template T of object
     * @param class-string<T> $class
     * @return T|null
     */
    private function findEntity(string $class, ?string $id): ?object
    {
        if (null === $id || '' === $id) {
            return null;
        }

        return $this->entityManager->find($class, $id);
    }

    private function isTrackedEmailMessage(mixed $message): bool
    {
        return $message instanceof SendEmailMessage;
    }

    private function resolveLog(?EmailLogStamp $stamp): ?EmailLog
    {
        if (null === $stamp || '' === $stamp->emailLogId) {
            return null;
        }

        $log = $this->emailLogs->find($stamp->emailLogId);

        return $log instanceof EmailLog ? $log : null;
    }

    private function resolveDirectLog(RawMessage $message): ?EmailLog
    {
        $logId = $this->directEmailLogIds[$message] ?? null;
        unset($this->directEmailLogIds[$message]);

        if (null === $logId) {
            return null;
        }

        $log = $this->emailLogs->find($logId);

        return $log instanceof EmailLog ? $log : null;
    }
}
