<?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\Controller\Page;

use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\AdminBundle\Breadcrumb\Breadcrumb;
use Ferienpass\AdminBundle\Controller\TagsResponses;
use Ferienpass\AdminBundle\Statistics\EditionStatsBuilder;
use Ferienpass\CmsBundle\Form\SimpleType\ContaoRequestTokenType;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Entity\Notification;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Export\Offer\OffersExportInterface;
use Ferienpass\CoreBundle\HttpCache\CacheTag;
use Ferienpass\CoreBundle\Message\LotEdition;
use Ferienpass\CoreBundle\Notification\EditionAwareNotificationInterface;
use Ferienpass\CoreBundle\Notifier\Notifier;
use Ferienpass\CoreBundle\Repository\EditionRepository;
use Ferienpass\CoreBundle\Repository\NotificationRepository;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Notifier\Notification\EmailNotificationInterface;
use Symfony\Component\Notifier\Recipient\Recipient;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\UX\Turbo\TurboBundle;

#[IsGranted('ROLE_ADMIN')]
#[Route('/saisons')]
final class EditionsController extends AbstractController
{
    use TagsResponses;

    #[Route('/{alias}/freigeben', name: 'admin_offers_review')]
    public function __invoke(#[MapEntity(mapping: ['alias' => 'alias'])] ?Edition $edition, #[CurrentUser] User $user, Request $request, Breadcrumb $breadcrumb, MessageBusInterface $bus, #[AutowireLocator(OffersExportInterface::class, indexAttribute: 'name', defaultIndexMethod: 'getName')] ServiceLocator $exporters): Response
    {
        if ($edition instanceof Edition) {
            $this->tagResponse(CacheTag::edition($edition));
        }

        /** @noinspection FormViewTemplate `createView()` messes ups error handling/redirect */
        return $this->render('@FerienpassAdmin/page/offers/reviewAll.html.twig', [
            'edition' => $edition,
            'pdfs' => array_map(static fn ($v) => substr($v, 0, -4), array_filter(array_keys($exporters->getProvidedServices()), static fn (string $k) => str_ends_with($k, '.pdf'))),
            'breadcrumb' => $breadcrumb->generate(['editions.title', ['route' => 'admin_editions_index']], [$edition?->getName() ?? '', ['route' => 'admin_editions_show', 'routeParameters' => ['alias' => $edition?->getAlias()]]]),
        ]);
    }

    #[Route('/{alias}/bulk-status', name: 'admin_offers_bulk_status')]
    public function bulkStatus(#[MapEntity(mapping: ['alias' => 'alias'])] Edition $edition): Response
    {
        $this->tagResponse(CacheTag::edition($edition));

        return $this->render('@FerienpassAdmin/page/offers/bulk_status.html.twig', [
            'edition' => $edition,
        ]);
    }

    #[Route('', name: 'admin_editions_index')]
    public function index(Breadcrumb $breadcrumb, EditionRepository $repository): Response
    {
        $qb = $repository->createQueryBuilder('i');

        // Join the holiday period so the list can be grouped (and ordered) by
        // the year it begins. Most recent year first; editions without a
        // holiday period sort last (NULL).
        $qb->leftJoin('i.tasks', 'holiday', 'WITH', "holiday.type = 'holiday'");
        $qb->addOrderBy('holiday.periodBegin', 'DESC');
        $qb->addOrderBy('i.name', 'ASC');

        return $this->render('@FerienpassAdmin/page/edition/index.html.twig', [
            'qb' => $qb,
            'createUrl' => $this->generateUrl('admin_editions_create'),
            'searchable' => ['name'],
            'breadcrumb' => $breadcrumb->generate('editions.title'),
        ]);
    }

    #[Route('/{alias}', name: 'admin_editions_show')]
    public function show(#[MapEntity(mapping: ['alias' => 'alias'])] Edition $edition, Breadcrumb $breadcrumb, EntityManagerInterface $em): Response
    {
        $this->tagResponse(CacheTag::edition($edition));

        // Statistics
        $hostsCount = $edition->getHosts()->count();
        $offersCount = $edition->getOffers()->count();

        $attendancesCount = $em->getRepository(Attendance::class)->createQueryBuilder('a')
            ->select('COUNT(a.id)')
            ->innerJoin('a.offer', 'o')
            ->where('o.edition = :edition')
            ->setParameter('edition', $edition)
            ->getQuery()
            ->getSingleScalarResult();

        // Average age
        $avgAge = $em->getRepository(Attendance::class)->createQueryBuilder('a')
            ->select('AVG(a.age)')
            ->innerJoin('a.offer', 'o')
            ->where('o.edition = :edition')
            ->andWhere('a.age IS NOT NULL')
            ->setParameter('edition', $edition)
            ->getQuery()
            ->getSingleScalarResult();

        return $this->render('@FerienpassAdmin/page/edition/show.html.twig', [
            'edition' => $edition,
            'hostsCount' => $hostsCount,
            'offersCount' => $offersCount,
            'attendancesCount' => $attendancesCount,
            'avgAge' => $avgAge ? round((float) $avgAge, 1) : null,
            'offers' => $edition->getOffers(),
            'breadcrumb' => $breadcrumb->generate(['editions.title', ['route' => 'admin_editions_index']], $edition->getName()),
        ]);
    }

    #[Route('/{alias}/statistik', name: 'admin_editions_stats')]
    public function stats(#[MapEntity(mapping: ['alias' => 'alias'])] Edition $edition, Request $request, Breadcrumb $breadcrumb, EditionStatsBuilder $statsBuilder, EditionRepository $editionRepository): Response
    {
        // Optional `?compare=<alias>` selects a comparison edition. We resolve
        // it here rather than in the builder so the controller stays the
        // single place URL parameters are interpreted.
        $compareTo = null;
        if ($compareAlias = $request->query->get('compare')) {
            $compareTo = $editionRepository->findOneBy(['alias' => $compareAlias]);
        }

        $this->tagResponse(CacheTag::edition($edition));
        // Tag the comparison edition too, so updates to either side invalidate
        // the side-by-side comparison view.
        if ($compareTo instanceof Edition) {
            $this->tagResponse(CacheTag::edition($compareTo));
        }

        return $this->render('@FerienpassAdmin/page/edition/stats.html.twig', [
            'edition' => $edition,
            'stats' => $statsBuilder->build($edition, $compareTo),
            'breadcrumb' => $breadcrumb->generate(['editions.title', ['route' => 'admin_editions_index']], [$edition->getName(), ['route' => 'admin_editions_show', 'routeParameters' => ['alias' => $edition->getAlias()]]]),
        ]);
    }

    #[Route('/{alias}/rundmail', name: 'admin_editions_mailing')]
    #[IsGranted('mailing', subject: 'edition')]
    public function mailing(#[MapEntity(mapping: ['alias' => 'alias'])] Edition $edition, Breadcrumb $breadcrumb): Response
    {
        $this->tagResponse(CacheTag::edition($edition));

        return $this->render('@FerienpassAdmin/page/edition/mailing.html.twig', [
            'edition' => $edition,
            'breadcrumb' => $breadcrumb->generate(['editions.title', ['route' => 'admin_editions_index']], [$edition->getName(), ['route' => 'admin_editions_show', 'routeParameters' => ['alias' => $edition->getAlias()]]]),
        ]);
    }

    #[Route('/{alias}/export', name: 'admin_editions_export')]
    #[IsGranted('export', subject: 'edition')]
    public function export(#[MapEntity(mapping: ['alias' => 'alias'])] Edition $edition, Breadcrumb $breadcrumb): Response
    {
        $this->tagResponse(CacheTag::edition($edition));

        return $this->render('@FerienpassAdmin/page/edition/export.html.twig', [
            'edition' => $edition,
            'breadcrumb' => $breadcrumb->generate(['editions.title', ['route' => 'admin_editions_index']], [$edition->getName(), ['route' => 'admin_editions_show', 'routeParameters' => ['alias' => $edition->getAlias()]]]),
        ]);
    }

    #[Route('/{alias}/benachrichtigungen/umschalten', name: 'admin_editions_notifications_toggle', methods: ['POST'])]
    public function notificationToggle(#[MapEntity(mapping: ['alias' => 'alias'])] Edition $edition, Request $request, Notifier $notifier, NotificationRepository $repository, EntityManagerInterface $em): Response
    {
        if (!$this->isCsrfTokenValid('notification_toggle', (string) $request->request->get('_token'))) {
            throw $this->createAccessDeniedException();
        }

        $type = (string) $request->request->get('type');
        if (!\in_array($type, $notifier->types(), true) || !is_subclass_of($notifier->getClass($type), EditionAwareNotificationInterface::class)) {
            throw $this->createNotFoundException();
        }

        $enable = $request->request->getBoolean('enable');
        $entity = $repository->findOneBy(['type' => $type, 'edition' => $edition]);

        if ($enable) {
            // Re-enabling: drop a bare "disabled" marker so the standard
            // template applies again, but keep a genuine content override.
            if ($entity instanceof Notification) {
                if ($this->notificationHasOverride($entity)) {
                    $entity->setDisabled(false);
                } else {
                    $em->remove($entity);
                }
            }
        } else {
            // Disabling: persist a marker row carrying the flag for this edition.
            if (!$entity instanceof Notification) {
                $entity = new Notification($type, $edition);
                $em->persist($entity);
            }

            $entity->setDisabled(true);
        }

        $em->flush();

        $this->addFlash('success', $enable ? 'Die Benachrichtigung wurde für diese Saison aktiviert.' : 'Die Benachrichtigung wurde für diese Saison deaktiviert.');

        return $this->redirectToRoute('admin_editions_notifications', ['alias' => $edition->getAlias(), 'type' => $type]);
    }

    #[Route('/{alias}/benachrichtigungen/zuruecksetzen', name: 'admin_editions_notifications_reset', methods: ['POST'])]
    public function notificationReset(#[MapEntity(mapping: ['alias' => 'alias'])] Edition $edition, Request $request, NotificationRepository $repository, EntityManagerInterface $em): Response
    {
        if (!$this->isCsrfTokenValid('notification_reset', (string) $request->request->get('_token'))) {
            throw $this->createAccessDeniedException();
        }

        $type = (string) $request->request->get('type');
        $entity = $repository->findOneBy(['type' => $type, 'edition' => $edition]);
        if ($entity instanceof Notification) {
            $em->remove($entity);
            $em->flush();
        }

        $this->addFlash('success', 'Die Vorlage wurde auf den Standard zurückgesetzt.');

        return $this->redirectToRoute('admin_editions_notifications', ['alias' => $edition->getAlias(), 'type' => $type]);
    }

    #[Route('/{alias}/benachrichtigungen/{type?}', name: 'admin_editions_notifications', methods: ['GET'])]
    public function notifications(#[MapEntity(mapping: ['alias' => 'alias'])] Edition $edition, ?string $type, Request $request, Breadcrumb $breadcrumb, Notifier $notifier, NotificationRepository $repository, TranslatorInterface $translator): Response
    {
        $editionAware = static fn (string $t): bool => is_subclass_of($notifier->getClass($t), EditionAwareNotificationInterface::class);

        $rowsByType = [];
        foreach ($repository->findBy(['edition' => $edition]) as $row) {
            $rowsByType[$row->getType()] = $row;
        }

        // All types appear in the nav; only edition-aware ones are configurable
        // per season — the rest are shown disabled (edited globally).
        $items = [];
        foreach ($notifier->types() as $t) {
            $isEa = $editionAware($t);
            $row = $isEa ? ($rowsByType[$t] ?? null) : null;
            $items[$t] = [
                'type' => $t,
                'editionAware' => $isEa,
                'enabled' => !($row instanceof Notification && $row->isDisabled()),
                'overridden' => $row instanceof Notification && $this->notificationHasOverride($row),
            ];
        }

        $editionAwareTypes = array_keys(array_filter($items, static fn (array $i): bool => $i['editionAware']));

        // Selected type must be edition-aware (the disabled types aren't previewable here).
        $selected = null !== $type && isset($items[$type]) && $items[$type]['editionAware'] ? $type : ($editionAwareTypes[0] ?? null);

        // Group the edition-aware types for the in-page master nav (order matters).
        $groupDefs = [
            'notifications.season.group.participation' => ['attendance_email_confirmation', 'attendance_completion', 'attendance_withdraw_confirmation', 'attendance_confirmed', 'attendance_newly_confirmed', 'attendance_withdrawn', 'attendance_decisions', 'remind_attendance'],
            'notifications.season.group.offer' => ['offer_cancelled', 'offer_relaunched'],
        ];
        $groups = [];
        $grouped = [];
        foreach ($groupDefs as $label => $keys) {
            $groupItems = [];
            foreach ($keys as $key) {
                if (isset($items[$key]) && $items[$key]['editionAware']) {
                    $groupItems[] = $items[$key];
                    $grouped[$key] = true;
                }
            }

            if ($groupItems) {
                $groups[] = ['label' => $label, 'items' => $groupItems];
            }
        }

        // Edition-aware stragglers, then the non-configurable (global) types.
        $eaRest = array_values(array_filter($items, static fn (array $i): bool => $i['editionAware'] && !isset($grouped[$i['type']])));
        if ($eaRest) {
            $groups[] = ['label' => 'notifications.season.group.other', 'items' => $eaRest];
        }

        $global = array_values(array_filter($items, static fn (array $i): bool => !$i['editionAware']));
        if ($global) {
            $groups[] = ['label' => 'notifications.season.group.global', 'items' => $global];
        }

        // "raw" renders the same branded email, but with the merge tokens shown
        // in place of the sample values (rather than a plain-text dump).
        $previewMode = 'raw' === $request->query->get('preview') ? 'raw' : 'rendered';

        $mockEmail = null;
        if (null !== $selected) {
            $row = $rowsByType[$selected] ?? null;
            $emailSubject = $row?->getEmailSubject() ?? $this->notificationDefault($translator, $selected, 'emailSubject');
            $emailText = $row?->getEmailText() ?? $this->notificationDefault($translator, $selected, 'emailText');

            $mock = $notifier->createMock($selected, $emailSubject ?? '', $emailText ?? '');
            if ($mock instanceof EmailNotificationInterface) {
                if ('raw' === $previewMode) {
                    $this->applyTokenPlaceholders($mock);
                }

                $mockEmail = $mock->asEmailMessage(new Recipient('erika.mustermann@example.com'));
            }
        }

        $this->tagResponse(CacheTag::edition($edition));

        return $this->render('@FerienpassAdmin/page/edition/notifications.html.twig', [
            'edition' => $edition,
            'groups' => $groups,
            'selectedType' => $selected,
            'selected' => null !== $selected ? $items[$selected] : null,
            'previewMode' => $previewMode,
            'mockEmail' => $mockEmail,
            'previewTo' => 'erika.mustermann@example.com',
            'breadcrumb' => $breadcrumb->generate(['editions.title', ['route' => 'admin_editions_index']], [$edition->getName(), ['route' => 'admin_editions_show', 'routeParameters' => ['alias' => $edition->getAlias()]]]),
        ]);
    }

    #[Route('/{alias}/löschen', name: 'admin_editions_delete')]
    public function delete(#[MapEntity(mapping: ['alias' => 'alias'])] Edition $edition, Request $request, EntityManagerInterface $entityManager): Response
    {
        $this->denyAccessUnlessGranted('delete', $edition);

        $form = $this->createForm(FormType::class, options: [
            'action' => $this->generateUrl('admin_editions_delete', ['alias' => $edition->getAlias()]),
        ]);

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $deletedId = $edition->getId();

            $entityManager->remove($edition);
            $entityManager->flush();

            if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) {
                // If the request comes from Turbo, set the content type as text/vnd.turbo-stream.html and only send the HTML to update
                $request->setRequestFormat(TurboBundle::STREAM_FORMAT);

                return $this->renderBlock('@FerienpassAdmin/page/edition/index.html.twig', 'deleted_stream', ['id' => $deletedId]);
            }

            return $this->redirectToRoute('admin_editions_index');
        }

        $this->tagResponse(CacheTag::edition($edition));

        return $this->render('@FerienpassAdmin/page/edition/delete.html.twig', [
            'item' => $edition,
            'form' => $form,
        ]);
    }

    #[Route('/{alias}/zuordnen', name: 'admin_editions_assign')]
    public function assign(#[MapEntity(mapping: ['alias' => 'alias'])] Edition $edition, Breadcrumb $breadcrumb): Response
    {
        $this->tagResponse(CacheTag::edition($edition));

        /** @noinspection FormViewTemplate `createView()` messes ups error handling/redirect */
        return $this->render('@FerienpassAdmin/page/offers/assignAll.html.twig', [
            'locked' => false,
            'edition' => $edition,
            'breadcrumb' => $breadcrumb->generate(['editions.title', ['route' => 'admin_editions_index']], [$edition->getName() ?? '', ['route' => 'admin_editions_show', 'routeParameters' => ['alias' => $edition->getAlias()]]]),
        ]);
    }

    #[Route('/{alias}/zuordnen/auto', name: 'admin_editions_lotting')]
    public function dispatchLotting(#[MapEntity(mapping: ['alias' => 'alias'])] Edition $edition, Request $request, MessageBusInterface $commandBus): Response
    {
        $this->denyAccessUnlessGranted('lotting', $edition);

        $form = $this->createFormBuilder(options: [
            'action' => $this->generateUrl('admin_editions_lotting', ['alias' => $edition->getAlias()]),
        ]);

        $form->add('requestToken', ContaoRequestTokenType::class);
        $form->add('waitlist', ChoiceType::class, [
            'label' => 'Überhang-Teilnehmende sollen…',
            'choices' => [
                '…auf die Warteliste' => 'waitlist',
                '…abgelehnt werden' => 'reject',
            ]])
        ;
        $form->add('ignoreMinParticipants', CheckboxType::class, [
            'label' => 'Auch Angebote zulosen, die die Mindestteilnahmezahl nicht erreichen',
            'required' => false,
        ]);

        $form = $form->getForm();
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $commandBus->dispatch(new LotEdition((string) $edition->getUuid(), 'waitlist' === $form->get('waitlist')->getData(), (bool) $form->get('ignoreMinParticipants')->getData()));

            if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) {
                $request->setRequestFormat(TurboBundle::STREAM_FORMAT);

                return $this->renderBlock('@FerienpassAdmin/page/offers/assignAll.html.twig', 'auto_success_stream');
            }

            return $this->redirectToRoute('admin_editions_assign', ['alias' => $edition->getAlias()]);
        }

        $this->tagResponse(CacheTag::edition($edition));

        return $this->render('@FerienpassAdmin/page/edition/lotting.html.twig', [
            'edition' => $edition,
            'form' => $form,
        ]);
    }

    private function notificationHasOverride(Notification $notification): bool
    {
        return null !== $notification->getEmailSubject()
            || null !== $notification->getEmailText()
            || null !== $notification->getSmsText()
            || null !== $notification->getEmailReplyTo()
            || null !== $notification->getEmailTo()
            || null !== $notification->getEmailAttachment();
    }

    private function notificationDefault(TranslatorInterface $translator, string $type, string $key): ?string
    {
        $transKey = \sprintf('defaults.%s.%s', $type, $key);
        $value = $translator->trans($transKey, domain: 'notifications');

        return $transKey !== $value ? $value : null;
    }

    /**
     * Rewrite the mock's sample entities in place so the rendered e-mail shows
     * the merge tokens ({@code {{ offer.name }}}) instead of sample values. The
     * entities are shared by reference with the notification context, so this
     * must run before {@see asEmailMessage()} captures the context.
     */
    private function applyTokenPlaceholders(object $notification): void
    {
        if (!method_exists($notification, 'getContext')) {
            return;
        }

        $context = $notification->getContext();
        $apply = static function (mixed $entity, array $map): void {
            if (!\is_object($entity)) {
                return;
            }

            foreach ($map as $setter => $value) {
                if (method_exists($entity, $setter)) {
                    $entity->{$setter}($value);
                }
            }
        };

        $apply($context['offer'] ?? null, ['setName' => '{{ offer.name }}', 'setDescription' => '{{ offer.description }}']);
        $apply($context['participant'] ?? null, ['setFirstname' => '{{ participant.firstname }}', 'setLastname' => '{{ participant.lastname }}']);
        $apply($context['user'] ?? null, ['setFirstname' => '{{ user.firstname }}', 'setLastname' => '{{ user.lastname }}', 'setEmail' => '{{ user.email }}', 'setPhone' => '{{ user.phone }}']);
        $apply($context['host'] ?? null, ['setName' => '{{ host.name }}', 'setEmail' => '{{ host.email }}']);
    }
}
