<?php
declare(strict_types=1);

namespace App\Service\Mailer;

use App\Entity\Message\ReportEmail;
use App\Enum\Core\EmailSendingTypeEnum;
use App\Enum\Message\MessageStatusEnum;
use App\Enum\Utils\EnvironnementVarEnum;
use App\Service\Mailer\Model\MailerModelInterface;
use App\Service\ServiceIterator\Mailer\BuilderIterator;
use App\Service\Utils\Environnement;
use App\Service\Utils\StringsUtils;
use App\Enum\Message\ReportMessageStatusEnum;
use App\Tests\Service\Mailer\MailerTest;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Email as SymfonyEmail;

/**
 * Classe Mailer : Service assurant l'envoi d'un mail à un destinataire
 */
class Mailer
{
    public function __construct(
        private MailerInterface $symfonyMailer,
        private string $opentalentNoReplyEmailAddress,
        private BuilderIterator $builderIterator,
        private StringsUtils $stringsUtils,
        private EntityManagerInterface $entityManager,
        private Environnement $environnement,
        private LoggerInterface $logger
    )
    {}

    /**
     * Main fonction qui itère les différentes étapes nécessaires à l'envoi d'un email
     *  - Le Build
     *  - Le Send
     *  - Le Reporting
     *
     * @param MailerModelInterface $mailerModel
     * @return ArrayCollection
     * @throws TransportExceptionInterface
     * @see MailerTest::testMain()
     */
    public function main(MailerModelInterface $mailerModel): ArrayCollection
    {
        // TODO: est-ce qu'on l'appellerait pas plutôt 'send' celle-ci? et est-ce qu'on ne passerait pas les autres en
        //       private? j'ai peur qu'on se trompe parfois entre l'une et l'autre

        $builderService = $this->builderIterator->getBuilderFor($mailerModel);
        $emailsCollection = $builderService->build($mailerModel);

        $emailsCollection = $this->reduceEmailsCollectionInPreproduction($emailsCollection);

        /** @var Email $email */
        foreach ($emailsCollection as $email){
            //si l'email n'a pas de destinataire, on ne l'envoi pas...
            if($email->getEmailRecipients()->isEmpty()){
                $email->getEmailEntity()->setStatus(MessageStatusEnum::NO_RECIPIENT()->getValue());
                continue;
            }

            //Envoi du mail
            $this->send($email);

            //Persistance de l'entité Email
            $this->persistEmailEntity($email);
        }

        //Envoi du rapport
        $this->sendReport($emailsCollection);

        $this->entityManager->flush();

        return $emailsCollection;
    }

    /**
     * Fonction d'envoi
     *
     * @param Email $email
     *
     * @throws TransportExceptionInterface
     * @see MailerTest::testSend()
     */
    public function send(Email $email): void{
        //On créé le mail
        $symfonyMail = $this->createSymfonyEmail($email);

        $this->addRecipients($symfonyMail, $email);

        $this->setAntiSpam($email, $symfonyMail->getTo());

        $this->setSymfonyEmailContent($symfonyMail, $email);

        $this->addHeaders($symfonyMail, $email);

        // @todo
        //$this->addAttachments();

        //On tente d'envoyer
        try {
            $this->symfonyMailer->send($symfonyMail);
            $email->getEmailEntity()->setStatus(MessageStatusEnum::SEND()->getValue());
            $email->getEmailEntity()->setDateSent(new \DateTime('now'));
        } catch (\Exception $e) {
            $this->logger->error('Error while sending email');
            $this->logger->error($e->getMessage());

            $email->getEmailEntity()->setStatus(MessageStatusEnum::FAILED()->getValue());
        }
    }

    /**
     * Envoi le rapport d'envoi
     * @param ArrayCollection<Email> $emails
     *
     * @see MailerTest::testSendReport()
     */
    public function sendReport(ArrayCollection $emails): void{
        $reportEmail = $this->createReportEmail($emails);
        try {
            $this->symfonyMailer->send($reportEmail);
        } catch (TransportExceptionInterface $e) {
            $this->logger->error('Error while sending report');
            $this->logger->error($e->getMessage());
        }
    }

    /**
     * Création du Mail qui sera envoyé via le Mailer de Symfony
     * @param Email $email
     * @return SymfonyEmail
     *
     * @see MailerTest::testCreateSymfonyEmail()
     */
    public function createSymfonyEmail(Email $email): SymfonyEmail
    {
        $addressMailFrom = new Address($email->getFrom(), $email->geFromName());

        return (new SymfonyEmail())
            ->from($addressMailFrom)
            ->replyTo($addressMailFrom)
            ->returnPath(Address::create("mail.report@opentalent.fr"))
            ->subject($email->getEmailEntity()->getAbout())
            ;
    }

    /**
     * @param SymfonyEmail $symfonyEmail
     * @param Email $email
     *
     * @see MailerTest::testSetSymfonyEmailContent()
     */
    public function setSymfonyEmailContent(SymfonyEmail $symfonyEmail, Email $email): void
    {
        $symfonyEmail
            ->html($email->getContent())
            ->text($this->stringsUtils->convertHtmlToText($email->getContent()));
    }

    /**
     * Créé le Templated Email contenant le rapport d'envoi
     * @param ArrayCollection<Email> $emails
     * @return TemplatedEmail
     *
     * @see MailerTest::testCreateReportEmail()
     */
    public function createReportEmail(ArrayCollection $emails): TemplatedEmail
    {
        [$delivered, $unDelivered] = $this->getDeliveredAndUndelivered($emails);

        /** @var Email $email */
        $email = $emails->first();
        return (new TemplatedEmail())
            ->from($this->opentalentNoReplyEmailAddress)
            ->subject(sprintf('Rapport d\'envoi du message : %s', $email->getEmailEntity()->getAbout()))
            ->htmlTemplate('@templates/emails/report.html.twig')
            ->context(
                [
                    'email_example' => $email->getEmailEntity(),
                    'delivered' => $delivered,
                    'unDelivered' => $unDelivered
                ]
            )
            ->addTo(new Address($email->getFrom(), $email->geFromName()))
        ;
    }

    /**
     * Récupère les recipient delivered & undelivered de l'ensemble des emails
     * @param ArrayCollection<Email> $emails
     * @return list<EmailRecipient[]>
     *
     * @see MailerTest::testGetDeliveredAndUndelivered()
     */
    function getDeliveredAndUndelivered(ArrayCollection $emails): array
    {
        $delivered = [];
        $unDelivered = [];

        foreach ($emails as $email){
            $emailRecipients = $email->getEmailRecipients();
            /** @var EmailRecipient $emailRecipient */
            foreach ($emailRecipients as $emailRecipient){
                if($emailRecipient->getSendStatus() === ReportMessageStatusEnum::MISSING()->getValue()){
                    $unDelivered[] = $emailRecipient;
                }else if($emailRecipient->getSendStatus() === ReportMessageStatusEnum::DELIVERED()->getValue()){
                    $delivered[] = $emailRecipient;
                }
            }
        }
        return [$delivered, $unDelivered];
    }

    /**
     * Persist l'Email
     * @param Email $email
     *
     * @see MailerTest::testPersistEmailEntity()
     */
    public function persistEmailEntity(Email $email): void
    {
        $emailEntity = $email->getEmailEntity();
        /** @var EmailRecipient $emailRecipient */
        foreach ($email->getEmailRecipients() as $emailRecipient){
            $emailEntity->addReport($this->createReport($emailRecipient));
        }
        $this->entityManager->persist($emailEntity);
    }

    /**
     * Création du rapport
     * @param EmailRecipient $emailRecipient
     *
     * @return ReportEmail
     * @see MailerTest::testCreateReport()
     */
    public function createReport(EmailRecipient $emailRecipient): ReportEmail
    {
        $reportEmail = new ReportEmail();
        $reportEmail
            ->setAddressEmail($emailRecipient->getEmailAddress())
            ->setAccess($emailRecipient->getAccess())
            ->setOrganization($emailRecipient->getOrganization())
            ->setDateSend(new \DateTime('now'))
            ->setStatus($emailRecipient->getSendStatus())
        ;
        //Return en deux temps car setStatus renvoi un AbstractReport et non un ReportEmail
        return $reportEmail;
    }

    /**
     * Reduit le nombre d'emails a envoyer si on ne se trouve pas en prod
     * @param ArrayCollection<Email> $emailsCollection
     *
     * @return ArrayCollection<Email>
     * @see MailerTest::testReduceEmailsCollectionInPreproduction()
     */
    public function reduceEmailsCollectionInPreproduction(ArrayCollection $emailsCollection): ArrayCollection
    {
        if($this->environnement->get(EnvironnementVarEnum::APP_ENV()->getValue()) !== 'prod' && $emailsCollection->count() > 20) {
            $startEmails = $emailsCollection->slice(0, 10);
            $endEmails = $emailsCollection->slice($emailsCollection->count() - 11, 10);
            return new ArrayCollection([...$startEmails, ...$endEmails]);
        }
        return $emailsCollection;
    }

    /**
     * @param SymfonyEmail $symfonyMail
     * @param Email $email
     *
     * @see MailerTest::testAddHeaders()
     */
    public function addHeaders(SymfonyEmail $symfonyMail, Email $email): void
    {
        $symfonyMail->getHeaders()->addTextHeader('List-Unsubscribe','mailto:'.$email->getFrom().'?subject=désabonnement');
        $symfonyMail->getHeaders()->addTextHeader('X-ID-OT', $email->getEmailEntity()->getUuid()->toString());
    }

    /**
     * On change le #__#ANTISPAM_PERSON_EMAIL#__# par la liste des emails afin d'éviter le spamming sur l'envoi en masse
     * @param Email $email
     * @param list<Address> $addresses
     * @see MailerTest::testSetAntiSpam()
     */
    public function setAntiSpam(Email $email, array $addresses): void
    {
        // map des Address pour ne conserver qu'un tableau d'email
        $to = array_map(function(Address $address){
            return $address->getAddress();
        }, $addresses);

        $email->setContent(str_replace('#__#ANTISPAM_PERSON_EMAIL#__#', implode(',', $to), $email->getContent()));
    }

    /**
     * On ajoute les destinataires suivant le type d'envoi souhaité
     *
     * @param SymfonyEmail $symfonyMail
     * @param Email $email
     * @see MailerTest::testAddRecipients()
     */
    public function addRecipients(SymfonyEmail $symfonyMail, Email $email): void{
        $allReadySend = [];

        foreach ($email->getEmailRecipients() as $emailRecipient){

            $addressMail = new Address($emailRecipient->getEmailAddress(), $emailRecipient->getName());

            //On envoi pas en double
            if(!in_array($addressMail, $allReadySend)){
                $allReadySend[] = $addressMail;

                switch($emailRecipient->getSendType()){
                    case EmailSendingTypeEnum::TO()->getValue():
                        $symfonyMail->addTo($addressMail);
                        break;
                    case EmailSendingTypeEnum::BBC()->getValue():
                        $symfonyMail->addBcc($addressMail);
                        break;
                    case EmailSendingTypeEnum::CC()->getValue():
                        $symfonyMail->addCc($addressMail);
                        break;
                }
            }
        }
    }
}
