<?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 Doctrine\Persistence\ManagerRegistry;
use Ferienpass\AdminBundle\Breadcrumb\Breadcrumb;
use Ferienpass\AdminBundle\Controller\TagsResponses;
use Ferienpass\AdminBundle\Export\ExportQueryBuilderInterface;
use Ferienpass\AdminBundle\Export\XlsxExportQueryBuilder;
use Ferienpass\AdminBundle\Form\EditParticipantType;
use Ferienpass\AdminBundle\Form\Filter\AttendancesFilter;
use Ferienpass\AdminBundle\Form\Filter\ParticipantFilter;
use Ferienpass\AdminBundle\Form\FormScreenFactory;
use Ferienpass\AdminBundle\Form\HandleFormTrait;
use Ferienpass\AdminBundle\List\ListCriteriaApplier;
use Ferienpass\AdminBundle\LiveComponent\MultiSelect;
use Ferienpass\AdminBundle\LiveComponent\MultiSelectAction;
use Ferienpass\AdminBundle\LiveComponent\MultiSelectHandlerInterface;
use Ferienpass\AdminBundle\Routing\Base58;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Entity\Participant\ParticipantInterface;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\HttpCache\CacheTag;
use Ferienpass\CoreBundle\Repository\AttendanceRepository;
use Ferienpass\CoreBundle\Repository\EditionRepository;
use Ferienpass\CoreBundle\Repository\ParticipantRepositoryInterface;
use Ferienpass\CoreBundle\Repository\PaymentRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapQueryParameter;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Requirement\Requirement;
use Symfony\Component\Security\Http\Attribute\IsGranted;
use Symfony\Component\Uid\Uuid;
use Symfony\UX\Turbo\TurboBundle;

#[IsGranted('ROLE_PARTICIPANTS_ADMIN')]
#[Route('/teilnehmende')]
final class ParticipantsController extends AbstractController implements MultiSelectHandlerInterface
{
    use HandleFormTrait;
    use TagsResponses;

    public function __construct(private readonly ManagerRegistry $doctrine, #[TaggedLocator(ExportQueryBuilderInterface::class, defaultIndexMethod: 'getFormat')] private readonly ServiceLocator $exporters)
    {
    }

    #[Route('/{edition?}', name: 'admin_participants_index', priority: -2, requirements: ['edition' => '[a-zA-Z0-9_\-+]+'])]
    public function index(?string $edition, ParticipantRepositoryInterface $repository, Breadcrumb $breadcrumb, EditionRepository $editions, XlsxExportQueryBuilder $xlsxExport): Response
    {
        $aliases = $edition ? array_values(array_filter(explode('+', $edition))) : [];
        $selectedEditions = $aliases ? $editions->findBy(['alias' => $aliases]) : [];
        if ($aliases && \count($selectedEditions) !== \count($aliases)) {
            throw $this->createNotFoundException();
        }

        /** @var User $user */
        $user = $this->getUser();
        $allEditions = $editions->findBy([], ['createdAt' => 'DESC']);
        $allEditions = $this->visibleEditions($allEditions, $user);

        $qb = $repository->createQueryBuilder('i');

        // Solve N+1
        $qb
            ->leftJoin('i.attendances', 'a')
            ->addSelect('a')
            ->leftJoin('a.offer', 'o')
            ->addSelect('o')
            ->leftJoin('i.activity', 'l')
            ->addSelect('l')
        ;

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

        return $this->render('@FerienpassAdmin/page/participants/index.html.twig', [
            'qb' => $qb,
            'filterType' => ParticipantFilter::class,
            'exports' => array_keys($this->exporters->getProvidedServices()),
            'searchable' => ['firstname', 'lastname', 'email', 'mobile', 'phone'],
            'createUrl' => $this->generateUrl('admin_participants_create'),
            'editions' => $allEditions,
            'selectedEditions' => $selectedEditions,
            'breadcrumb' => $breadcrumb->generate('participants.title'),
        ]);
    }

    #[Route('.{format}', name: 'admin_participants_export', requirements: ['format' => '\w+'])]
    #[Route('/{edition}.{format}', name: 'admin_participants_export_in_edition', requirements: ['edition' => '[a-zA-Z0-9_\-+]+', 'format' => '\w+'])]
    public function export(?string $edition, ParticipantRepositoryInterface $repository, string $format, Request $request, ListCriteriaApplier $filterer, EditionRepository $editions)
    {
        $qb = $repository->createQueryBuilder('i');

        if (null !== $edition) {
            $aliases = array_values(array_filter(explode('+', $edition)));
            $selectedEditions = $aliases ? $editions->findBy(['alias' => $aliases]) : [];
            if ($aliases && \count($selectedEditions) !== \count($aliases)) {
                throw $this->createNotFoundException();
            }
            if ([] !== $selectedEditions) {
                $qb->leftJoin('i.attendances', 'a')
                   ->leftJoin('a.offer', 'o')
                   ->andWhere('o.edition IN (:selectedEditions)')
                   ->setParameter('selectedEditions', $selectedEditions);
            }
        }

        $filterer->apply(
            $qb,
            ParticipantFilter::class,
            ['firstname', 'lastname', 'email', 'mobile', 'phone'],
            $request->query->get('q'),
            $request->query->get('sort'),
            (array) $request->query->all('filter')
        );

        if (!$this->exporters->has($format)) {
            throw $this->createNotFoundException();
        }

        $exporter = $this->exporters->get($format);

        return $this->file($exporter->generate($qb), "teilnehmende.$format");
    }

    #[Route('/neu/{section?default}', name: 'admin_participants_create')]
    #[Route('/{uuid}/bearbeiten/{section?default}', name: 'admin_participants_edit', requirements: ['uuid' => Base58::UUID], priority: -1)]
    #[Route('/{uuid}/bearbeiten/{section?default}', name: 'admin_participants_edit_legacy', requirements: ['uuid' => Requirement::UUID], priority: -1)]
    public function edit(?Uuid $uuid, string $section, Request $request, \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher, EntityManagerInterface $entityManager, Breadcrumb $breadcrumb, ParticipantRepositoryInterface $repository, EntityManagerInterface $em, FormScreenFactory $forms): Response
    {
        if (null !== $uuid && !($entity = $repository->findByUuid($uuid)) instanceof ParticipantInterface) {
            throw $this->createNotFoundException();
        }

        $entity ??= $repository->createNew();

        $form = $this->createForm(EditParticipantType::class, $entity, $formOptions = [
            'display_section' => $section,
        ]);

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $this->handleSubmit($form, $dispatcher, $request);

            $entityManager->flush();

            $this->addFlash('success', 'Die Änderungen wurden erfolgreich übernommen.');

            if ($nextSection = $request->request->get('nextSection')) {
                return $this->redirectToRoute('admin_participants_edit', ['uuid' => $entity->getUuid()->toBase58(), 'section' => $nextSection]);
            }

            if ($redirect = $request->request->get('redirect')) {
                return $this->redirectToRoute($redirect, ['uuid' => $entity->getUuid()->toBase58()]);
            }

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

        $breadcrumbTitle = $entity->getId() ? $entity->getName() : 'participants.new';

        $this->tagResponse(CacheTag::participant($entity));

        return $this->render('@FerienpassAdmin/page/participants/edit.html.twig', [
            'item' => $entity,
            'form' => $form,
            'formOptions' => $formOptions,
            'formScreen' => $forms->screen(EditParticipantType::class, $entity),
            'section' => $section,
            'breadcrumb' => $breadcrumb->generate(['participants.title', ['route' => 'admin_participants_index']], $breadcrumbTitle),
        ]);
    }

    #[Route('/{uuid}', name: 'admin_participants_show', requirements: ['uuid' => Base58::UUID])]
    #[Route('/{uuid}', name: 'admin_participants_show_legacy', requirements: ['uuid' => Requirement::UUID])]
    public function show(
        Uuid $uuid,
        ParticipantRepositoryInterface $repository,
        PaymentRepository $paymentRepository,
        EditionRepository $editions,
        #[MapQueryParameter(name: 'kommentare')]
        ?bool $commentsOnly,
        #[MapQueryParameter(name: 'saison')]
        ?string $saisonAlias,
        Breadcrumb $breadcrumb,
    ): Response {
        if (!($entity = $repository->findByUuid($uuid)) instanceof ParticipantInterface) {
            throw $this->createNotFoundException();
        }

        $this->denyAccessUnlessGranted('view', $entity);

        $this->tagResponse(CacheTag::participant($entity));

        // `saison=<edition-alias>` scopes the show page to a single Saison —
        // entry point used from OfferAssign so the side panel only surfaces
        // the attendances + payments that belong to the offer's edition. The
        // dob/account banner and the "Aktivität" section are also suppressed
        // in that mode (those are about the participant as a whole, not the
        // selected Saison). A provided-but-unknown alias 404s rather than
        // silently falling back, so a stale link surfaces the mismatch
        // instead of misleading the reviewer with the unscoped view.
        $saison = null;
        if (null !== $saisonAlias) {
            $saison = $editions->findOneBy(['alias' => $saisonAlias]);
            if (null === $saison) {
                throw $this->createNotFoundException(\sprintf('Edition with alias "%s" not found.', $saisonAlias));
            }
        }

        return $this->render('@FerienpassAdmin/page/participants/show.html.twig', [
            'item' => $entity,
            'payments' => $paymentRepository->findByParticipant($entity),
            'commentsOnly' => (bool) $commentsOnly,
            // Template uses the DB-side name; the public `?saison=` query
            // param stays untouched (user-facing label).
            'edition' => $saison,
            'breadcrumb' => $breadcrumb->generate(['participants.title', ['route' => 'admin_participants_index']], $entity->getName()),
        ]);
    }

    #[Route('/{uuid}/anmeldungen', name: 'admin_participants_attendances', requirements: ['uuid' => Base58::UUID])]
    #[Route('/{uuid}/anmeldungen', name: 'admin_participants_attendances_legacy', requirements: ['uuid' => Requirement::UUID])]
    public function attendances(Uuid $uuid, ParticipantRepositoryInterface $participantRepository, AttendanceRepository $attendanceRepository, Request $request, Breadcrumb $breadcrumb): Response
    {
        /** @var ParticipantInterface $participant */
        $participant = $participantRepository->findByUuid($uuid);
        if (null === $participant) {
            throw $this->createNotFoundException();
        }

        $qb = $attendanceRepository->createQueryBuilder('i');
        $qb
            ->leftJoin('i.offer', 'o')
            ->leftJoin('o.edition', 'e')
            ->leftJoin('o.dates', 'd')
            ->innerJoin('i.participant', 'p')
            ->leftJoin('i.activity', 'l')
            ->addSelect('l')
            ->where('p.id = :participant')
            ->setParameter('participant', $participant->getId())
        ;

        /** @var Attendance[] $items */
        $items = $qb->getQuery()->getResult();

        $ms = new MultiSelect([
            new MultiSelectAction('settle', 'admin_attendances_settle', method: 'GET', turboFrame: 'slideover'),
            new MultiSelectAction('changeStatus', 'admin_participants_attendances_status', method: 'GET', turboFrame: 'slideover'),
        ], self::class, array_values(array_map(static fn (Attendance $a) => $a->getId(), array_filter($items, static fn (Attendance $a) => $a->isPayable() && !$a->isPaid()))));

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

        return $this->render('@FerienpassAdmin/page/participants/attendances.html.twig', [
            'qb' => $qb,
            'filterType' => AttendancesFilter::class,
            'ms' => $ms,
            'searchable' => ['o.name'],
            'participant' => $participant,
            'breadcrumb' => $breadcrumb->generate(['participants.title', ['route' => 'admin_participants_index']], $participant->getName()),
        ]);
    }

    public function handleMultiSelect(string $action, array $selected, Request $request): Response
    {
        throw new \RuntimeException('Code should not be reached');
    }

    #[Route('/{uuid}/löschen', name: 'admin_participants_delete', requirements: ['uuid' => Base58::UUID])]
    #[Route('/{uuid}/löschen', name: 'admin_participants_delete_legacy', requirements: ['uuid' => Requirement::UUID])]
    public function delete(Uuid $uuid, ParticipantRepositoryInterface $repository, Request $request, EntityManagerInterface $entityManager): Response
    {
        /** @var ParticipantInterface $item */
        $item = $repository->findByUuid($uuid);
        if (null === $item) {
            throw $this->createNotFoundException();
        }

        $this->denyAccessUnlessGranted('delete', $item);

        $form = $this->createForm(FormType::class, options: [
            'action' => $this->generateUrl('admin_participants_delete', ['uuid' => $uuid->toBase58()]),
        ]);
        $form->handleRequest($request);

        // TODO Can use a facade
        if ($form->isSubmitted() && $form->isValid()) {
            foreach ($item->getAttendances() as $attendance) {
                foreach ($attendance->getPaymentItems() as $paymentItem) {
                    $paymentItem->removeAttendanceAssociation();
                }
            }

            $deletedId = $item->getId();

            $entityManager->remove($item);
            $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/participants/index.html.twig', 'deleted_stream', ['id' => $deletedId]);
            }

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

        $this->tagResponse(CacheTag::participant($item));

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

    #[Route('/anmeldung/{uuid}', name: 'admin_attendance_show', requirements: ['uuid' => Base58::UUID])]
    #[Route('/anmeldung/{uuid}', name: 'admin_attendance_show_legacy', requirements: ['uuid' => Requirement::UUID])]
    public function showAttendance(Uuid $uuid, AttendanceRepository $attendanceRepository, #[MapQueryParameter(name: 'kommentare')] ?bool $commentsOnly): Response
    {
        $attendance = $attendanceRepository->findOneBy(['uuid' => $uuid]);
        if (null === $attendance) {
            throw $this->createNotFoundException();
        }

        // Tag with both the attendance and its participant (and the offer,
        // since the show template displays offer context). All three xkeys
        // map through CacheInvalidationListener cascades.
        $this->tagResponse([
            CacheTag::attendance($attendance),
            CacheTag::participant($attendance->getParticipant()),
            CacheTag::offer($attendance->getOffer()),
        ]);

        return $this->render('@FerienpassAdmin/page/attendances/show.html.twig', [
            'item' => $attendance,
            'commentsOnly' => (bool) $commentsOnly,
        ]);
    }

    #[Route('/anmeldung/{uuid}/zahllast', name: 'admin_attendance_pay_status', requirements: ['uuid' => Base58::UUID])]
    #[Route('/anmeldung/{uuid}/zahllast', name: 'admin_attendance_pay_status_legacy', requirements: ['uuid' => Requirement::UUID])]
    public function attendance(Uuid $uuid, AttendanceRepository $attendanceRepository, Request $request, EntityManagerInterface $entityManager, \Psr\EventDispatcher\EventDispatcherInterface $dispatcher): Response
    {
        $attendance = $attendanceRepository->findOneBy(['uuid' => $uuid]);
        if (null === $attendance) {
            throw $this->createNotFoundException();
        }

        $form = $this->createForm(FormType::class, options: [
            'action' => $this->generateUrl('admin_attendance_pay_status', [
                'uuid' => $attendance->getUuid()->toBase58(),
            ]),
        ]);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $attendance->setPayable(!$attendance->isPayable());

            $entityManager->flush();

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

        $this->tagResponse(CacheTag::participant($attendance->getParticipant()));

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

    #[Route('/anmeldungen/status', name: 'admin_participants_attendances_status')]
    public function attendanceStatus(#[MapQueryParameter('ids')] array $ids, #[MapQueryParameter] bool $stashed = false): Response
    {
        return $this->render('@FerienpassAdmin/page/participants/attendance_status.html.twig', [
            'selected' => $ids,
            'stashed' => $stashed,
        ]);
    }

    #[Route('/anmeldung/{uuid}/zahlungen', name: 'admin_participants_attendances_payments', requirements: ['uuid' => Base58::UUID], defaults: ['_requires_payments' => true])]
    #[Route('/anmeldung/{uuid}/zahlungen', name: 'admin_participants_attendances_payments_legacy', requirements: ['uuid' => Requirement::UUID], defaults: ['_requires_payments' => true])]
    public function attendancePayments(Uuid $uuid, AttendanceRepository $attendanceRepository): Response
    {
        $attendance = $attendanceRepository->findOneBy(['uuid' => $uuid]);
        if (null === $attendance) {
            throw $this->createNotFoundException();
        }

        return $this->render('@FerienpassAdmin/page/participants/attendance_payments.html.twig', [
            'attendance' => $attendance,
        ]);
    }

    #[Route('/abrechnen', name: 'admin_attendances_settle')]
    public function settle(Request $request): Response
    {
        $ids = (array) $request->query->all('ids');

        if (empty($ids)) {
            $session = $request->getSession();
            if ($session->has('ms.settle')) {
                $ids = $session->get('ms.settle');
            }
        }

        return $this->render('@FerienpassAdmin/page/participants/settle.html.twig', [
            'ids' => $ids,
        ]);
    }

    /**
     * @param Edition[] $editions
     *
     * @return Edition[]
     */
    private function visibleEditions(array $editions, User $user): array
    {
        if ($this->isGranted('ROLE_ADMIN')) {
            return $editions;
        }
        $userHost = $user->getHosts()->first() ?: null;

        return array_values(array_filter($editions, static fn (Edition $e) => $e->getHosts()->isEmpty() || ($userHost && $e->getHosts()->contains($userHost))));
    }
}
