<?php

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\EventBundle\Controller;

use Chill\EventBundle\Entity\Event;
use Chill\EventBundle\Entity\Participation;
use Chill\EventBundle\Form\ParticipationType;
use Chill\EventBundle\Repository\EventRepository;
use Chill\EventBundle\Security\ParticipationVoter;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\Common\Collections\Collection;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatorInterface;

/**
 * Class ParticipationController.
 */
final class ParticipationController extends AbstractController
{
    /**
     * ParticipationController constructor.
     */
    public function __construct(
        private readonly LoggerInterface $logger,
        private readonly TranslatorInterface $translator,
        private readonly EventRepository $eventRepository,
        private readonly PersonRepository $personRepository,
        private readonly \Doctrine\Persistence\ManagerRegistry $managerRegistry,
    ) {}

    #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/participation/create', name: 'chill_event_participation_create')]
    public function createAction(Request $request): Response|\Symfony\Component\HttpFoundation\RedirectResponse
    {
        // test the request is correct
        try {
            $this->testRequest($request);
        } catch (\RuntimeException $ex) {
            $this->logger->warning($ex->getMessage());

            return (new Response())
                ->setStatusCode(Response::HTTP_BAD_REQUEST)
                ->setContent($ex->getMessage());
        }

        // forward to other action
        $single = $request->query->has('person_id');
        $multiple = $request->query->has('persons_ids');

        if (true === $single) {
            return $this->createSingle($request);
        }

        if (true === $multiple) {
            return $this->createMultiple($request);
        }

        // at this point, we miss the required fields. Throw an error
        return (new Response())
            ->setStatusCode(Response::HTTP_BAD_REQUEST)
            ->setContent("You must provide either 'person_id' or "
                        ."'persons_ids' argument in query");
    }

    /**
     * @param null $return_path
     */
    public function createCreateForm(Participation $participation, $return_path = null): FormInterface
    {
        $form = $this->createForm(ParticipationType::class, $participation, [
            'event_type' => $participation->getEvent()->getType(),
            'action' => $this->generateUrl('chill_event_participation_create', [
                'return_path' => $return_path,
                'event_id' => $participation->getEvent()->getId(),
                'person_id' => $participation->getPerson()->getId(),
            ]),
        ]);

        $form->add('submit', SubmitType::class, [
            'label' => 'Create',
        ]);

        return $form;
    }

    public function createCreateFormMultiple(array $participations): FormInterface
    {
        $form = $this->createForm(
            \Symfony\Component\Form\Extension\Core\Type\FormType::class,
            ['participations' => $participations],
            [
                'action' => $this->generateUrl(
                    'chill_event_participation_create',
                    [
                        'event_id' => current($participations)->getEvent()->getId(),
                        'persons_ids' => implode(',', array_map(
                            static fn (Participation $p) => $p->getPerson()->getId(),
                            $participations
                        )),
                    ]
                ), ]
        );
        $form->add(
            'participations',
            CollectionType::class,
            [
                'entry_type' => ParticipationType::class,
                'entry_options' => [
                    'event_type' => current($participations)->getEvent()->getType(),
                ],
            ]
        );

        $form->add('submit', SubmitType::class, [
            'label' => 'Create',
        ]);

        return $form;
    }

    public function createEditForm(Participation $participation): FormInterface
    {
        $form = $this->createForm(
            ParticipationType::class,
            $participation,
            [
                'event_type' => $participation->getEvent()->getType(),
                'action' => $this->generateUrl(
                    'chill_event_participation_update',
                    [
                        'participation_id' => $participation->getId(),
                    ]
                ),
            ]
        );

        $form->add(
            'submit',
            SubmitType::class,
            [
                'label' => 'Edit',
            ]
        );

        return $form;
    }

    public function createMultiple(Request $request): Response|\Symfony\Component\HttpFoundation\RedirectResponse
    {
        $participations = $this->handleRequest($request, new Participation(), true);

        foreach ($participations as $participation) {
            $this->denyAccessUnlessGranted(
                ParticipationVoter::CREATE,
                $participation,
                'The user is not allowed to create this participation'
            );
        }

        $form = $this->createCreateFormMultiple($participations);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $em = $this->managerRegistry->getManager();
            $data = $form->getData();

            foreach ($data['participations'] as $participation) {
                $em->persist($participation);
            }

            $em->flush();

            $this->addFlash('success', $this->translator->trans(
                'The participations were created'
            ));

            return $this->redirectToRoute('chill_event__event_show', [
                'event_id' => $participations[0]->getEvent()->getId(),
            ]);
        }

        return $this->render('@ChillEvent/Participation/new.html.twig', [
            'form' => $form->createView(),
            'participation' => $participation,
        ]);
    }

    public function createSingle(Request $request): Response|\Symfony\Component\HttpFoundation\RedirectResponse
    {
        $participation = $this->handleRequest($request, new Participation(), false);

        $this->denyAccessUnlessGranted(
            ParticipationVoter::CREATE,
            $participation,
            'The user is not allowed to create this participation'
        );

        $form = $this->createCreateForm($participation);
        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $em = $this->managerRegistry->getManager();

            $em->persist($participation);
            $em->flush();

            $this->addFlash('success', $this->translator->trans(
                'The participation was created'
            ));

            if ($request->query->has('return_path')) {
                return $this->redirect($request->query->get('return_path'));
            }

            return $this->redirectToRoute('chill_event__event_show', [
                'id' => $participation->getEvent()->getId(),
            ]);
        }

        return $this->render('@ChillEvent/Participation/new.html.twig', [
            'form' => $form->createView(),
            'participation' => $participation,
            'ignored_participations' => [],
        ]);
    }

    /**
     * @param int $participation_id
     */
    #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/participation/{participation_id}/delete', name: 'chill_event_participation_delete', requirements: ['participation_id' => '\d+'])]
    public function deleteAction($participation_id, Request $request): Response|\Symfony\Component\HttpFoundation\RedirectResponse
    {
        $em = $this->managerRegistry->getManager();
        $participation = $em->getRepository(Participation::class)->findOneBy([
            'id' => $participation_id,
        ]);

        if (!$participation) {
            throw $this->createNotFoundException('Unable to find participation.');
        }

        /** @var Event $event */
        $event = $participation->getEvent();

        $form = $this->createDeleteForm($participation_id);

        if (Request::METHOD_POST === $request->getMethod()) {
            $form->handleRequest($request);

            if ($form->isSubmitted() && $form->isValid()) {
                $em->remove($participation);
                $em->flush();

                $this->addFlash(
                    'success',
                    $this->translator
                        ->trans('The participation has been sucessfully removed')
                );

                return $this->redirectToRoute('chill_event__event_show', [
                    'id' => $event->getId(),
                ]);
            }
        }

        return $this->render('@ChillEvent/Participation/confirm_delete.html.twig', [
            'event_id' => $event->getId(),
            'delete_form' => $form->createView(),
        ]);
    }

    /**
     * Show an edit form for the participation with the given id.
     *
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException          if the participation is not found
     * @throws \Symfony\Component\HttpFoundation\File\Exception\AccessDeniedException if the user is not allowed to edit the participation
     */
    #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/participation/{participation_id}/edit', name: 'chill_event_participation_edit')]
    public function editAction(int $participation_id): Response
    {
        /** @var Participation $participation */
        $participation = $this->managerRegistry->getManager()
            ->getRepository(Participation::class)
            ->find($participation_id);

        if (null === $participation) {
            throw $this->createNotFoundException('The participation is not found');
        }

        $this->denyAccessUnlessGranted(
            ParticipationVoter::UPDATE,
            $participation,
            'You are not allowed to edit this participation'
        );

        $form = $this->createEditForm($participation);

        return $this->render('@ChillEvent/Participation/edit.html.twig', [
            'form' => $form->createView(),
            'participation' => $participation,
        ]);
    }

    /**
     * show a form to edit multiple participation for the same event.
     *
     * @param int $event_id
     */
    #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/participation/{event_id}/edit_multiple', name: 'chill_event_participation_edit_multiple')]
    public function editMultipleAction($event_id): Response|\Symfony\Component\HttpFoundation\RedirectResponse
    {
        $event = $this->managerRegistry->getRepository(Event::class)
            ->find($event_id);

        if (null === $event) {
            throw $this->createNotFoundException("The event with id {$event_id} is not found");
        }

        // check for ACL, on Event level and on Participation Level
        $this->denyAccessUnlessGranted('CHILL_EVENT_SEE', $event, 'You are not allowed '
                .'to see this event');

        foreach ($event->getParticipations() as $participation) {
            $this->denyAccessUnlessGranted(
                ParticipationVoter::UPDATE,
                $participation,
                'You are not allowed to update participation with id '.$participation->getId()
            );
        }

        switch ($event->getParticipations()->count()) {
            case 0:
                // if there aren't any participation, redirect to the 'show' view with an add flash
                $this->addFlash('warning', $this->translator
                    ->trans('There are no participation to edit for this event'));

                return $this->redirectToRoute(
                    'chill_event__event_show',
                    ['event_id' => $event->getId()]
                );

            case 1:
                // redirect to the form for a single participation
                return $this->redirectToRoute('chill_event_participation_edit', [
                    'participation_id' => $event->getParticipations()->current()->getId(),
                ]);
        }

        $form = $this->createEditFormMultiple($event->getParticipations(), $event);

        return $this->render('@ChillEvent/Participation/edit-multiple.html.twig', [
            'event' => $event,
            'participations' => $event->getParticipations(),
            'form' => $form->createView(),
        ]);
    }

    /**
     * Show a form to add a participation.
     *
     * This function parse the person_id / persons_ids query argument
     * and decide if it should process a single or multiple participation. Depending
     * on this, the appropriate layout and form.
     */
    #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/participation/new', name: 'chill_event_participation_new')]
    public function newAction(Request $request): Response|\Symfony\Component\HttpFoundation\RedirectResponse
    {
        // test the request is correct
        try {
            $this->testRequest($request);
        } catch (\RuntimeException $ex) {
            $this->logger->warning($ex->getMessage());

            return (new Response())
                ->setStatusCode(Response::HTTP_BAD_REQUEST)
                ->setContent($ex->getMessage());
        }

        // forward to other action
        $single = $request->query->has('person_id');
        $multiple = $request->query->has('persons_ids');

        if (true === $single) {
            return $this->newSingle($request);
        }

        if (true === $multiple) {
            return $this->newMultiple($request);
        }

        // at this point, we miss the required fields. Throw an error
        return (new Response())
            ->setStatusCode(Response::HTTP_BAD_REQUEST)
            ->setContent("You must provide either 'person_id' or "
                        ."'persons_ids' argument in query");
    }

    #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/participation/{participation_id}/update', name: 'chill_event_participation_update', methods: ['POST'])]
    public function updateAction(int $participation_id, Request $request): Response
    {
        /** @var Participation $participation */
        $participation = $this->managerRegistry->getManager()
            ->getRepository(Participation::class)
            ->find($participation_id);

        if (null === $participation) {
            throw $this->createNotFoundException('The participation is not found');
        }

        $this->denyAccessUnlessGranted(
            ParticipationVoter::UPDATE,
            $participation,
            'You are not allowed to edit this participation'
        );

        $form = $this->createEditForm($participation);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $em = $this->managerRegistry->getManager();

            $em->flush();

            $this->addFlash('success', $this->translator->trans(
                'The participation was updated'
            ));

            return $this->redirectToRoute('chill_event__event_show', [
                'id' => $participation->getEvent()->getId(),
            ]);
        }

        return $this->render('@ChillEvent/Participation/edit.html.twig', [
            'form' => $form->createView(),
            'participation' => $participation,
        ]);
    }

    #[\Symfony\Component\Routing\Annotation\Route(path: '/{_locale}/event/participation/{event_id}/update_multiple', name: 'chill_event_participation_update_multiple', methods: ['POST'])]
    public function updateMultipleAction(mixed $event_id, Request $request)
    {
        /** @var Event $event */
        $event = $this->managerRegistry->getRepository(Event::class)
            ->find($event_id);

        if (null === $event) {
            throw $this->createNotFoundException("The event with id {$event_id} is not found");
        }

        $this->denyAccessUnlessGranted('CHILL_EVENT_SEE', $event, 'You are not allowed '
                .'to see this event');

        foreach ($event->getParticipations() as $participation) {
            $this->denyAccessUnlessGranted(
                ParticipationVoter::UPDATE,
                $participation,
                'You are not allowed to update participation with id '.$participation->getId()
            );
        }

        $form = $this->createEditFormMultiple($event->getParticipations(), $event);

        $form->handleRequest($request);

        if ($form->isSubmitted() && $form->isValid()) {
            $this->managerRegistry->getManager()->flush();

            $this->addFlash('success', $this->translator->trans('The participations '
                    .'have been successfully updated.'));

            return $this->redirectToRoute(
                'chill_event__event_show',
                ['id' => $event->getId()]
            );
        }

        return $this->render('@ChillEvent/Participation/edit-multiple.html.twig', [
            'event' => $event,
            'participations' => $event->getParticipations(),
            'form' => $form->createView(),
        ]);
    }

    /**
     * @return FormInterface
     */
    protected function createEditFormMultiple(Collection $participations, Event $event)
    {
        $form = $this->createForm(
            \Symfony\Component\Form\Extension\Core\Type\FormType::class,
            ['participations' => $participations],
            [
                'method' => 'POST',
                'action' => $this->generateUrl('chill_event_participation_update_multiple', [
                    'event_id' => $event->getId(),
                ]),
            ]
        );

        $form->add(
            'participations',
            CollectionType::class,
            [
                'entry_type' => ParticipationType::class,
                'entry_options' => [
                    'event_type' => $event->getType(),
                ],
            ]
        );

        $form->add('submit', SubmitType::class, [
            'label' => 'Update',
        ]);

        return $form;
    }

    /**
     * Handle the request to adapt $participation.
     *
     * If the request is multiple, the $participation object is cloned.
     * Limitations: the $participation should not be persisted.
     *
     * @return Participation|list<Participation> return one single participation if $multiple == false
     *
     * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException    if the event/person is not found
     * @throws \Symfony\Component\Security\Core\Exception\AccessDeniedException if the user does not have access to event/person
     */
    protected function handleRequest(
        Request $request,
        Participation $participation,
        bool $multiple = false,
    ): array|Participation {
        $em = $this->managerRegistry->getManager();

        if ($em->contains($participation)) {
            throw new \LogicException('The participation object should not be managed by the object manager using the method '.__METHOD__);
        }

        $event_id = $request->query->getInt('event_id', 0); // sf4 check:

        $event = $this->eventRepository->find($event_id);

        if (null === $event) {
            throw $this->createNotFoundException('The event with id '.$event_id.' is not found');
        }

        $this->denyAccessUnlessGranted(
            'CHILL_EVENT_SEE',
            $event,
            'The user is not allowed to see the event'
        );

        $participation->setEvent($event);

        // this script should be able to handle multiple, so we translate
        // single person_id in an array
        $persons_ids = $request->query->has('person_id') ?
            [$request->query->get('person_id', 0)]
            : explode(',', (string) $request->query->get('persons_ids'));
        $participations = [];

        foreach ($persons_ids as $person_id) {
            // clone if we have to reuse the $participation
            $participation = \count($persons_ids) > 1 ? clone $participation : $participation;

            if (null !== $person_id) {
                $person = $this->personRepository->find($person_id);

                if (null === $person) {
                    throw $this->createNotFoundException('The person with id '.$person_id.' is not found');
                }

                $this->denyAccessUnlessGranted(
                    PersonVoter::SEE,
                    $person,
                    'The user is not allowed to see the person'
                );

                $participation->setPerson($person);
            }

            $participations[] = $participation;
        }

        return $multiple ? $participations : $participations[0];
    }

    /**
     * Show a form with multiple participation.
     *
     * If a person is already participating on the event (if a participation with
     * the same person is associated with the event), the participation is ignored.
     *
     * If all but one participation is ignored, the page show the same response
     * than the newSingle function.
     *
     * If all participations must be ignored, an error is shown and the method redirects
     * to the event 'show' view with an appropriate flash message.
     */
    protected function newMultiple(Request $request): Response|\Symfony\Component\HttpFoundation\RedirectResponse
    {
        $participations = $this->handleRequest($request, new Participation(), true);
        $ignoredParticipations = $newParticipations = [];

        foreach ($participations as $participation) {
            /* @var Participation $participation */
            // check for authorization
            $this->denyAccessUnlessGranted(
                ParticipationVoter::CREATE,
                $participation,
                'The user is not allowed to create this participation'
            );

            // create a collection of person's id participating to the event
            $peopleParticipating ??= $participation->getEvent()->getParticipations()->map(
                static fn (Participation $p) => $p->getPerson()->getId()
            );
            // check that the user is not already in the event
            if ($peopleParticipating->contains($participation->getPerson()->getId())) {
                $ignoredParticipations[] = $participation
                    ->getEvent()->getParticipations()->filter(
                        static fn (Participation $p) => $p->getPerson()->getId() === $participation->getPerson()->getId()
                    )->first();
            } else {
                $newParticipations[] = $participation;
            }
        }

        // this is where the function redirect depending on valid participation

        if ([] === $newParticipations) {
            // if we do not have nay participants, redirect to event view
            $this->addFlash('error', $this->translator->trans(
                'None of the requested people may participate '
                    .'the event: they are maybe already participating.'
            ));

            return $this->redirectToRoute('chill_event__event_show', [
                'event_id' => $request->query->getInt('event_id', 0),
            ]);
        }

        if (\count($newParticipations) > 1) {
            // if we have multiple participations, show a form with multiple participations
            $form = $this->createCreateFormMultiple($newParticipations);

            return $this->render(
                '@ChillEvent/Participation/new-multiple.html.twig',
                [
                    'form' => $form->createView(),
                    'participations' => $newParticipations,
                    'ignored_participations' => $ignoredParticipations,
                ]
            );
        }

        // if we have only one participation, show the same form than for single participation
        $form = $this->createCreateForm($participation);

        return $this->render(
            '@ChillEvent/Participation/new.html.twig',
            [
                'form' => $form->createView(),
                'participation' => $participation,
                'ignored_participations' => $ignoredParticipations,
            ]
        );
    }

    /**
     * Show a form with single participation.
     *
     * @return Response
     */
    protected function newSingle(Request $request)
    {
        $returnPath = $request->query->has('return_path') ?
                $request->query->get('return_path') : null;

        $participation = $this->handleRequest($request, new Participation(), false);

        $this->denyAccessUnlessGranted(
            ParticipationVoter::CREATE,
            $participation,
            'The user is not allowed to create this participation'
        );

        $form = $this->createCreateForm($participation, $returnPath);

        return $this->render('@ChillEvent/Participation/new.html.twig', [
            'form' => $form->createView(),
            'participation' => $participation,
            'ignored_participations' => [], // this is required, see self::newMultiple
        ]);
    }

    /**
     * Test that the query parameters are valid :.
     *
     * - an `event_id` is existing ;
     * - `person_id` and `persons_ids` are **not** both present ;
     * - `persons_id` is correct (contains only numbers and a ','.
     *
     * @throws \RuntimeException if an error is detected
     */
    protected function testRequest(Request $request)
    {
        $single = $request->query->has('person_id');
        $multiple = $request->query->has('persons_ids');

        if (true === $single && true === $multiple) {
            // we are not allowed to have both person_id and persons_ids
            throw new \RuntimeException("You are not allow to provide both 'person_id' and 'persons_ids' simulaneously");
        }

        if (true === $multiple) {
            $persons_ids = $request->query->get('persons_ids');

            if (!preg_match('/^([0-9]{1,},{0,1}){1,}[0-9]{0,}$/', (string) $persons_ids)) {
                throw new \RuntimeException('The persons_ids value should '."contains int separated by ','");
            }
        }

        // check for event_id - this could be removed later
        if (false === $request->query->has('event_id')) {
            throw new \RuntimeException('You must provide an event_id');
        }
    }

    /**
     * @return FormInterface
     */
    private function createDeleteForm($participation_id)
    {
        return $this->createFormBuilder()
            ->setAction($this->generateUrl('chill_event_participation_delete', [
                'participation_id' => $participation_id,
            ]))
            ->add('submit', SubmitType::class, ['label' => 'Delete'])
            ->getForm();
    }
}
