<?php

declare(strict_types=1);

/*
 * This file is part of the Ferienpass package.
 *
 * (c) Richard Henkenjohann <richard@ferienpass.online>
 *
 * For more information visit the project website <https://ferienpass.online>
 * or the documentation under <https://docs.ferienpass.online>.
 */

namespace Ferienpass\AdminBundle\Components;

use Doctrine\ORM\QueryBuilder;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Facade\PaymentsFacade;
use Ferienpass\CoreBundle\Message\DispatchMailing;
use Ferienpass\CoreBundle\Messenger\UuidStamp;
use Ferienpass\CoreBundle\Payments\Provider\PaymentProviderInterface;
use Ferienpass\CoreBundle\Repository\EditionRepository;
use Ferienpass\CoreBundle\Repository\HostRepository;
use Ferienpass\CoreBundle\Repository\OfferRepositoryInterface;
use Ferienpass\CoreBundle\Repository\ParticipantRepositoryInterface;
use Ferienpass\CoreBundle\Repository\SentEmailRepository;
use Ferienpass\CoreBundle\Repository\UserRepository;
use Ferienpass\CoreBundle\Session\Flash;
use Ferienpass\CoreBundle\Twig\SandboxedTextRenderer;
use League\CommonMark\CommonMarkConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\String\Slugger\SluggerInterface;
use Symfony\Component\Translation\TranslatableMessage;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveArg;
use Symfony\UX\LiveComponent\Attribute\LiveListener;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\ComponentToolsTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;
use Symfony\UX\LiveComponent\Metadata\UrlMapping;
use Symfony\UX\LiveComponent\ValidatableComponentTrait;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;
use Twig\Environment;
use Twig\Error\Error;

#[AsLiveComponent(route: 'live_component_admin')]
class Mailing extends AbstractController
{
    use ComponentToolsTrait;
    use DefaultActionTrait;
    use ValidatableComponentTrait;

    #[LiveProp(writable: true, url: new UrlMapping(as: 'gruppe'))]
    public ?string $group = null;

    #[LiveProp]
    public ?Edition $edition = null;

    #[LiveProp(writable: true)]
    public bool $hostsNotDisabled = true;

    #[LiveProp(writable: true)]
    public bool $onlyParticipantsUnacknowledged = false;

    #[LiveProp(writable: true)]
    public bool $onlyParticipantsUnpaid = false;

    #[LiveProp(writable: true)]
    public bool $addPaymentLink = false;

    #[LiveProp(writable: true)]
    public bool $hostsWithOffer = false;

    #[LiveProp(writable: true, url: new UrlMapping(as: 'saison'))]
    public array $selectedEditions = [];

    #[LiveProp(writable: true, onUpdated: 'onOffersUpdated', url: new UrlMapping(as: 'angebote'))]
    public array $selectedOffers = [];

    #[LiveProp(writable: true, url: new UrlMapping(as: 'veranstaltende'))]
    public array $selectedHosts = [];

    #[LiveProp(writable: true)]
    #[Assert\NotBlank()]
    public string $emailSubject = '';

    #[LiveProp(writable: true)]
    #[Assert\NotBlank()]
    public string $emailText = '';

    #[LiveProp]
    public ?string $emailAttachment = null;

    #[LiveProp(writable: true)]
    public array $attendanceStatus = [];

    #[LiveProp]
    public bool $showConfirm = false;

    #[LiveProp]
    public array $removedEmails = [];

    public ?TranslatableMessage $confirm = null;

    public function __construct(private readonly EditionRepository $editions, private readonly ParticipantRepositoryInterface $participants, private readonly UserRepository $users, private readonly HostRepository $hosts, private readonly OfferRepositoryInterface $offers, private readonly Environment $twig, private readonly RequestStack $requestStack, private readonly NormalizerInterface $normalizer, private readonly MessageBusInterface $commandBus, private readonly SluggerInterface $slugger, private readonly PaymentProviderInterface $paymentProvider, private readonly SentEmailRepository $sentEmails, private readonly SandboxedTextRenderer $sandboxedTextRenderer)
    {
    }

    public function mount(?Edition $edition = null): void
    {
        $this->edition = $edition;
        if ($this->edition) {
            $this->selectedEditions = [$this->edition->getId()];
            $this->hostsWithOffer = true;
        }

        if (!$this->isGranted('ROLE_ADMIN')) {
            $this->group = 'veranstaltende';
        }

        if ('veranstaltende' === $this->group) {
            $this->hostsWithOffer = true;
        }
    }

    #[LiveListener('group')]
    public function changeGroup(#[LiveArg] string $group)
    {
        $this->group = $group;

        if ('veranstaltende' === $this->group) {
            $this->hostsWithOffer = true;
        }
    }

    public function onOffersUpdated($previous): void
    {
        if ($this->isGranted('ROLE_ADMIN')) {
            return;
        }

        $this->selectedOffers = $previous;
    }

    #[ExposeInTemplate]
    public function countAllHosts()
    {
        $qb = $this->users->createQueryBuilder('u')
            ->innerJoin('u.hostAssociations', 'ha')
            ->innerJoin('ha.host', 'host')
        ;

        if ($this->hostsNotDisabled) {
            $qb->andWhere('u.disable = :disabled')->setParameter('disabled', 0);
        }

        if ($this->selectedEditions) {
            $qb->innerJoin('host.offers', 'offers');
            $qb->leftJoin('offers.edition', 'edition');
            $qb->andWhere('edition IN (:editions)')->setParameter('editions', $this->selectedEditions);
        }

        $qb->select('COUNT(DISTINCT u.email)');

        return $qb->getQuery()->getSingleScalarResult();
    }

    #[ExposeInTemplate]
    public function countAllParticipants()
    {
        $qb = $this->participants->createQueryBuilder('participant')
            ->innerJoin('participant.attendances', 'attendance')
            ->innerJoin('attendance.offer', 'offer')
            ->leftJoin('offer.edition', 'edition')
            ->leftJoin('participant.user', 'u')
        ;

        if ($this->selectedEditions) {
            $qb->andWhere('edition IN (:editions)')->setParameter('editions', $this->selectedEditions);
        }

        $qb->select('COUNT(DISTINCT COALESCE(participant.email, u.email))');

        return $qb->getQuery()->getSingleScalarResult();
    }

    #[ExposeInTemplate]
    public function context(): array
    {
        $context = [];

        $context['baseUrl'] = $this->requestStack->getCurrentRequest()?->getSchemeAndHttpHost().$this->requestStack->getCurrentRequest()?->getBaseUrl();

        if (1 === \count($this->selectedEditions)) {
            $context['edition'] = $this->editions->find(array_values($this->selectedEditions)[0]);
        }
        if (1 === \count($this->selectedOffers)) {
            $context['offer'] = $this->offers->find(array_values($this->selectedOffers)[0]);
        }
        if (1 === \count($this->selectedHosts)) {
            $context['host'] = $this->hosts->find(array_values($this->selectedHosts)[0]);
        }

        return $context;
    }

    #[ExposeInTemplate]
    public function editionOptions()
    {
        return $this->editions->findBy(['archived' => 0]);
    }

    #[ExposeInTemplate]
    public function offerOptions()
    {
        $qb = $this->offers->createQueryBuilder('o')
            ->where('o.id IN (:ids)')
            ->setParameter('ids', $this->selectedOffers);

        if (!$this->isGranted('ROLE_ADMIN')) {
            $user = $this->getUser();
            $qb->innerJoin('o.hosts', 'hosts')->andWhere('hosts IN (:hosts)')->setParameter('hosts', $user instanceof User ? $user->getHosts() : []);
        }

        return $qb->getQuery()->getResult();
    }

    #[ExposeInTemplate]
    public function hostOptions()
    {
        $qb = $this->hosts->createQueryBuilder('h')
            ->where('h.id IN (:ids)')
            ->setParameter('ids', $this->selectedHosts)
        ;

        if (!$this->isGranted('ROLE_ADMIN')) {
            $user = $this->getUser();
            $qb->andWhere('h.id IN (:hosts)')->setParameter('hosts', $user instanceof User ? $user->getHosts() : []);
        }

        return $qb->getQuery()->getResult();
    }

    #[ExposeInTemplate]
    public function recipients(): array
    {
        $return = [];

        if ('veranstaltende' === $this->group) {
            foreach ($this->queryHostAccounts() as $item) {
                $return[$item->getEmail()][] = $item;
            }
        } elseif ('teilnehmende' === $this->group) {
            foreach ($this->queryParticipants() as $item) {
                $return[$item->getEmail()][] = $item;
            }
        }

        return array_diff_key($return, array_flip($this->removedEmails));
    }

    #[ExposeInTemplate]
    public function preview(): string|false
    {
        try {
            return $this->sandboxedTextRenderer->render($this->parsedEmailText(), $this->context());
        } catch (Error) {
            return false;
        }
    }

    #[ExposeInTemplate]
    public function canCreatePaymentLinks(): bool
    {
        return $this->paymentProvider->isConfigured();
    }

    #[ExposeInTemplate]
    public function availableTokens(): array
    {
        $availableTokens = [];

        foreach ($this->context() as $token => $object) {
            switch ($token) {
                case 'baseUrl':
                    $availableTokens[$token] = $object;
                    break;
                case 'edition':
                case 'offer':
                case 'host':
                    $tokens = $this->normalizer->normalize($object, context: ['groups' => 'notification']);
                    foreach (array_keys((array) $tokens) as $property) {
                        $availableTokens["$token.$property"] = $this->container->get('twig')->createTemplate(\sprintf('{{ %s }}', "$token.$property"))->render([$token => $tokens]);
                    }
                    break;
            }
        }

        return $availableTokens + ['recipientEmail' => 'empfaenger@beispiel.de'];
    }

    #[LiveAction]
    public function submit(Request $request, ValidatorInterface $validator, Flash $flash)
    {
        $this->validate();

        $file = $request->files->get('emailAttachment');
        if ($file instanceof UploadedFile) {
            // This LiveAction bypasses the Symfony form (and therefore its File
            // constraints), so the uploaded attachment MUST be validated
            // explicitly here BEFORE it is moved to disk and attached to the
            // bulk mailing. The allow-list mirrors the offer AttachmentManager:
            // PDF only, max 6 MB.
            $violations = $validator->validate($file, new Assert\File(
                maxSize: '6M',
                mimeTypes: ['application/pdf'],
                mimeTypesMessage: 'Erlaubte Dateitypen sind: PDF',
            ));

            if ($violations->count() > 0) {
                $flash->addError(text: $violations->get(0)->getMessage());

                return;
            }

            $this->emailAttachment = $this->upload($file);
        }

        $this->showConfirm = true;
    }

    #[LiveListener('sendMailing')]
    public function send(): Response
    {
        $uuid = Uuid::v7();

        $command = new DispatchMailing(
            $this->getUser()?->getUuid()?->toRfc4122(),
            array_keys($this->recipients()),
            $this->emailSubject,
            $this->parsedEmailText(),
            $this->context(),
            array_filter([$this->emailAttachment]),
            withPaymentLink: $this->onlyParticipantsUnacknowledged && $this->addPaymentLink,
            offerIds: $this->selectedOffers,
            editionIds: $this->selectedEditions,
        );
        $this->commandBus->dispatch(
            new Envelope($command)->with(new UuidStamp($uuid))
        );

        $this->confirm = new TranslatableMessage('mailing.confirm', domain: 'admin');
        $this->showConfirm = false;
        $this->resetValidation();

        return $this->redirectToRoute('admin_tools_dispatch', ['uuid' => $uuid->toBase58()]);
    }

    #[LiveAction]
    public function remove(#[LiveArg] string $email): void
    {
        $this->removedEmails[] = $email;
    }

    #[ExposeInTemplate]
    public function recentMailings(): array
    {
        $qb0 = $this->sentEmails->createQueryBuilder('e');

        $qb = $this->sentEmails->createQueryBuilder('email')
            ->innerJoin('email.logEntry', 'logEntry')
            ->innerJoin('logEntry.group', 'groupLog')
            ->where($qb0->expr()->isNotNull('logEntry.group'))
            ->andWhere($qb0->expr()->eq('logEntry.message', ':messageType'))
            ->setParameter('messageType', \Ferienpass\CoreBundle\Message\SendMailing::class);

        if ($this->edition) {
            $qb->leftJoin('groupLog.offers', 'offer')
                ->leftJoin('groupLog.editions', 'edition')
                ->andWhere($qb->expr()->orX(
                    'offer.edition = :edition',
                    'edition = :edition'
                ))
                ->setParameter('edition', $this->edition);
        }

        $list = $qb->groupBy('logEntry.group')
            ->orderBy('logEntry.createdAt', 'DESC')
            ->setMaxResults(10)
            ->getQuery()
            ->getResult();

        if ($this->isGranted('ROLE_ADMIN')) {
            return $list;
        }

        return array_values(array_filter($list, function ($message) {
            if (!$message instanceof \Ferienpass\CoreBundle\Entity\SentMessage) {
                return false;
            }
            $log = $message->getLogEntry();
            if (null === $log) {
                return false;
            }
            $groupLog = $log->getGroup();
            if (null !== $groupLog) {
                foreach ($groupLog->getEditions() as $edition) {
                    if ($this->isGranted('view', $edition)) {
                        return true;
                    }
                }
                foreach ($groupLog->getOffers() as $offer) {
                    $edition = $offer->getEdition();
                    if ($edition && $this->isGranted('view', $edition)) {
                        return true;
                    }
                }
                foreach ($groupLog->getAttendances() as $attendance) {
                    $offer = $attendance->getOffer();
                    $edition = $offer?->getEdition();
                    if ($edition && $this->isGranted('view', $edition)) {
                        return true;
                    }
                }
            }

            return false;
        }));
    }

    private function upload(UploadedFile $file): string
    {
        $originalFilename = pathinfo($file->getClientOriginalName(), \PATHINFO_FILENAME);
        $safeFilename = $this->slugger->slug($originalFilename);
        $fileName = $safeFilename.'-'.uniqid().'.'.$file->guessExtension();

        try {
            $file->move(sys_get_temp_dir(), $fileName);
        } catch (FileException) {
            // ... handle exception if something happens during file upload
        }

        return sys_get_temp_dir().'/'.$fileName;
    }

    private function queryHostAccounts()
    {
        $qb = $this->users
            ->createQueryBuilder('u')
            ->innerJoin('u.hostAssociations', 'ha')
            ->innerJoin('ha.host', 'host')
        ;

        if ($this->hostsNotDisabled) {
            $qb->andWhere('u.disable = :disabled')->setParameter('disabled', 0);
        }

        if ($this->hostsWithOffer) {
            $qb->innerJoin('host.offers', 'offers');
            $qb->leftJoin('offers.edition', 'edition');

            if ($this->selectedEditions) {
                $qb->andWhere('edition IN (:editions)')->setParameter('editions', $this->selectedEditions);
            }
        }

        if ($this->selectedHosts) {
            $qb->andWhere('host IN (:hosts)')->setParameter('hosts', $this->selectedHosts);
        }

        return $qb->getQuery()->getResult();
    }

    private function parsedEmailText(): string
    {
        $converter = new CommonMarkConverter([
            'html_input' => 'strip',
            'allow_unsafe_links' => false,
            'renderer' => [
                'soft_break' => "<br />\n",
            ],
        ]);

        return (string) $converter->convert($this->emailText);
    }

    private function queryParticipants()
    {
        /** @var QueryBuilder $qb */
        $qb = $this->participants->createQueryBuilder('participant');
        $qb
            ->innerJoin('participant.attendances', 'attendance')
            ->innerJoin('attendance.offer', 'offer')
            ->leftJoin('offer.edition', 'edition')
        ;

        if (!$this->isGranted('ROLE_ADMIN')) {
            $user = $this->getUser();
            $qb->innerJoin('offer.hosts', 'hosts')->andWhere('hosts IN (:hosts)')->setParameter('hosts', $user instanceof User ? $user->getHosts() : []);
        }

        if ($this->selectedOffers) {
            $qb->andWhere('offer IN (:offers)')->setParameter('offers', $this->selectedOffers);
        }

        if ($this->selectedEditions) {
            $qb->andWhere('edition IN (:editions)')->setParameter('editions', $this->selectedEditions);
        }

        if ($this->attendanceStatus) {
            $qb->andWhere('attendance.status IN (:status)')->setParameter('status', $this->attendanceStatus);
        }

        if ($this->onlyParticipantsUnacknowledged) {
            PaymentsFacade::addWhereLogic($qb);

            if ($this->onlyParticipantsUnpaid) {
                $qb->andWhere('offer.fee > 0');
            }
        }

        return $qb->getQuery()->getResult();
    }
}
