<?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 Contao\CoreBundle\Exception\PageNotFoundException;
use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\AdminBundle\Breadcrumb\Breadcrumb;
use Ferienpass\AdminBundle\Controller\TagsResponses;
use Ferienpass\AdminBundle\Form\EditOfferType;
use Ferienpass\AdminBundle\Form\FormScreenFactory;
use Ferienpass\AdminBundle\Form\HandleFormTrait;
use Ferienpass\AdminBundle\Routing\Base58;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\OfferDate;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\HttpCache\CacheTag;
use Ferienpass\CoreBundle\Repository\OfferRepositoryInterface;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Requirement\Requirement;
use Symfony\Component\Uid\Uuid;
use Symfony\Component\Workflow\WorkflowInterface;
use Symfony\UX\Turbo\TurboBundle;

#[Route('/angebote')]
final class OfferEditController extends AbstractController
{
    use HandleFormTrait;
    use TagsResponses;

    public function __construct(
        private readonly OfferRepositoryInterface $offers,
        private readonly EntityManagerInterface $entityManager,
        private readonly ServiceLocator $sectionComponents,
    ) {
    }

    #[Route('/{uuid}/bearbeiten/{section?default}', name: 'admin_offers_edit', requirements: ['uuid' => Base58::UUID], priority: -2)]
    #[Route('/{uuid}/bearbeiten/{section?default}', name: 'admin_offers_edit_legacy', requirements: ['uuid' => Requirement::UUID], priority: -2)]
    #[Route('/neu/leer', name: 'admin_offers_create')]
    #[Route('/{edition}/neu/leer', name: 'admin_offers_create_in_edition')]
    #[Route('/{uuid}/kopieren', name: 'admin_offers_copy', requirements: ['uuid' => Base58::UUID], priority: -1)]
    #[Route('/{uuid}/kopieren', name: 'admin_offers_copy_legacy', requirements: ['uuid' => Requirement::UUID], priority: -1)]
    #[Route('/{edition}/{uuid}/kopieren', name: 'admin_offers_copy_to_edition', requirements: ['uuid' => Base58::UUID], priority: -1)]
    #[Route('/{edition}/{uuid}/kopieren', name: 'admin_offers_copy_to_edition_legacy', requirements: ['uuid' => Requirement::UUID], priority: -1)]
    #[Route('/{uuid}/variante/{section?default}', name: 'admin_offers_new_variant', requirements: ['uuid' => Base58::UUID], priority: -1)]
    #[Route('/{uuid}/variante/{section?default}', name: 'admin_offers_new_variant_legacy', requirements: ['uuid' => Requirement::UUID], priority: -1)]
    public function __invoke(?Uuid $uuid, #[MapEntity(mapping: ['edition' => 'alias'])] ?Edition $edition, Request $request, Breadcrumb $breadcrumb, FormScreenFactory $forms, EventDispatcherInterface $dispatcher, EntityManagerInterface $entityManager, WorkflowInterface $offerWorkflow, ?string $section = null): Response
    {
        $offer = $this->getOffer($uuid, $edition, $request, $offerWorkflow);
        $section ??= 'default';

        $isCopy = \in_array($request->attributes->get('_canonical_route', $request->get('_route')), ['admin_offers_copy', 'admin_offers_copy_to_edition', 'admin_offers_new_variant'], true);

        $formScreen = $forms->screen(EditOfferType::class, $offer)
            ->withExtraSections(array_keys($this->sectionComponents->getProvidedServices()))
            // 'applicationForm' and 'contact' moved from form-collection fields
            // to LiveComponents (section components), which would otherwise
            // drift to the end of the nav. Pin them back after 'agreementLetter';
            // unlisted sections keep their natural order after these.
            ->withSectionOrder(['default', 'dates', 'location', 'details', 'applications', 'agreementLetter', 'applicationForm', 'contact']);

        $hasComponent = $this->sectionComponents->has($section);
        if (!$hasComponent && !\in_array($section, $formScreen->sections(), true)) {
            throw new PageNotFoundException('Section not found');
        }

        if ($hasComponent) {
            $this->tagResponse(CacheTag::offer($offer));

            return $this->render('@FerienpassAdmin/page/offers/edit.html.twig', [
                'item' => $offer,
                'section' => $section,
                'isCopy' => $isCopy,
                'formScreen' => $formScreen,
                'isReadonly' => $offer->getId() && !$this->isGranted('edit', $offer),
                'component_class' => $this->sectionComponents->get($section)::class,
                'breadcrumb' => $breadcrumb->generate(
                    ['offers.title', ['route' => 'admin_offers_index', 'routeParameters' => array_filter(['edition' => $offer->getEdition()?->getAlias()])]],
                    $offer->getEdition() instanceof Edition ? [$offer->getEdition()->getName(), ['route' => 'admin_offers_index', 'routeParameters' => ['edition' => $offer->getEdition()->getAlias()]]] : null,
                    $offer->getName()
                ),
            ]);
        }

        $form = $this->createForm(EditOfferType::class, $offer, $formOptions = [
            'is_variant' => !$offer->isVariantBase(),
            'display_section' => $section,
            'disabled' => $isReadonly = $offer->getId() && !$this->isGranted('edit', $offer),
        ]);

        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $oldMaxParticipants = $this->entityManager->getUnitOfWork()->getOriginalEntityData($offer)['maxParticipants'] ?? null;

            $this->handleSubmit($form, $dispatcher, $request);

            $this->entityManager->flush();

            $nextSection = $request->request->get('nextSection');
            $redirect = $request->request->get('redirect');

            if (!$nextSection && !$redirect && TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) {
                $maxParticipantsChanged = $oldMaxParticipants !== $offer->getMaxParticipants();

                $request->setRequestFormat(TurboBundle::STREAM_FORMAT);

                return $this->renderBlock('@FerienpassAdmin/page/offers/edit.html.twig', 'success_stream', [
                    'item' => $offer,
                    'reorder' => $maxParticipantsChanged && $offer->getMaxParticipants() > $offer->getAttendancesConfirmed()->count() && !$offer->getAttendancesWaitlisted()->isEmpty() && !$offer->isPast(),
                    'overbooked' => null !== $offer->getMaxParticipants() && $offer->getAttendancesConfirmed()->count() > $offer->getMaxParticipants(),
                ]);
            }

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

            if ($nextSection) {
                return $this->redirectToRoute('admin_offers_edit', ['uuid' => $offer->getUuid()->toBase58(), 'section' => $nextSection]);
            }

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

            if ($isCopy) {
                return $this->redirectToRoute('admin_offers_edit', ['uuid' => $offer->getUuid()->toBase58()]);
            }

            if ($offer->getEdition() instanceof Edition) {
                return $this->redirectToRoute('admin_offers_index', ['edition' => $offer->getEdition()->getAlias()]);
            }

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

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

        return $this->render('@FerienpassAdmin/page/offers/edit.html.twig', [
            'item' => $offer,
            'form' => $form,
            'section' => $section,
            'isCopy' => $isCopy,
            'isReadonly' => $isReadonly,
            'formOptions' => $formOptions,
            'formScreen' => $formScreen,
            'breadcrumb' => $breadcrumb->generate(
                ['offers.title', ['route' => 'admin_offers_index', 'routeParameters' => array_filter(['edition' => $offer->getEdition()?->getAlias()])]],
                $offer->getEdition() instanceof Edition ? [$offer->getEdition()->getName(), ['route' => 'admin_offers_index', 'routeParameters' => ['edition' => $offer->getEdition()->getAlias()]]] : null,
                $offer->getName() ?? ''
            ),
        ]);
    }

    #[Route('/termin/{uuid}', name: 'admin_offer_date_edit', requirements: ['uuid' => Base58::UUID])]
    #[Route('/termin/{uuid}', name: 'admin_offer_date_edit_legacy', requirements: ['uuid' => Requirement::UUID])]
    public function editDate(Uuid $uuid): Response
    {
        $date = $this->entityManager->getRepository(OfferDate::class)->findOneBy(['uuid' => $uuid]);
        if (null === $date) {
            throw $this->createNotFoundException();
        }

        $this->denyAccessUnlessGranted('edit', $date->getOffer());

        // Tag with the parent offer's UUID so CacheInvalidationListener's
        // OfferDate → CacheTag::offer($offer) cascade actually purges this
        // page when the date is edited via the OfferDateForm LiveComponent.
        // Without this tag, Varnish has no link between the cached HTML and
        // the offer, and the page keeps serving the pre-edit form values
        // until s-maxage expires (1 year).
        $this->tagResponse(CacheTag::offer($date->getOffer()));

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

    #[Route('/{uuid}/termin/neu', name: 'admin_offer_date_add', requirements: ['uuid' => Base58::UUID])]
    #[Route('/{uuid}/termin/neu', name: 'admin_offer_date_add_legacy', requirements: ['uuid' => Requirement::UUID])]
    public function addDate(Uuid $uuid): Response
    {
        $offer = $this->offers->findOneByUuid($uuid);
        if (!$offer) {
            throw $this->createNotFoundException();
        }

        // Same rationale as editDate(): the LiveComponent will persist on
        // submit without re-checking permissions.
        $this->denyAccessUnlessGranted('edit', $offer);

        $date = new OfferDate($offer);

        // Same rationale as editDate(): a new date is persisted via the
        // LiveComponent save(), which cascades to the offer tag — but only
        // pages tagged with that offer actually get purged.
        $this->tagResponse(CacheTag::offer($offer));

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

    #[Route('/termin/{uuid}/loeschen', name: 'admin_offer_date_delete', requirements: ['uuid' => Base58::UUID])]
    #[Route('/termin/{uuid}/loeschen', name: 'admin_offer_date_delete_legacy', requirements: ['uuid' => Requirement::UUID])]
    public function deleteDate(Uuid $uuid, Request $request): Response
    {
        $date = $this->entityManager->getRepository(OfferDate::class)->findOneBy(['uuid' => $uuid]);
        if (null === $date) {
            throw $this->createNotFoundException();
        }

        $offer = $date->getOffer();
        $this->denyAccessUnlessGranted('delete', $offer);

        // Tag the delete-confirmation GET form for the same reason as above:
        // if the date or its parent offer changes between the user opening
        // this form and confirming the delete, a stale form would let them
        // submit against an out-of-date snapshot.
        $this->tagResponse(CacheTag::offer($offer));

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

        if ($form->isSubmitted() && $form->isValid()) {
            $deletedUuid = (string) $date->getUuid();
            $offer->getDates()->removeElement($date);
            $this->entityManager->remove($date);
            $this->entityManager->flush();

            $isLastDate = $offer->getDates()->isEmpty();

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

                return $this->renderBlock('@FerienpassAdmin/page/offers/delete_date.html.twig', 'success_stream', [
                    'offer' => $offer,
                    'deletedUuid' => $deletedUuid,
                    'isLastDate' => $isLastDate,
                ]);
            }

            return $this->redirectToRoute('admin_offers_edit', ['uuid' => $offer->getUuid()->toBase58(), 'section' => 'dates']);
        }

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

    private function getOffer(?Uuid $uuid, ?Edition $edition, Request $request, WorkflowInterface $offerWorkflow): OfferInterface
    {
        $user = $this->getUser();

        $offer = null;
        if (null !== $uuid) {
            /** @var OfferInterface $offer */
            $offer = $this->offers->findOneBy(['uuid' => $uuid]);
        }

        if ('admin_offers_edit' === $request->attributes->get('_canonical_route', $request->get('_route')) && null === $offer) {
            throw new PageNotFoundException('Item not found');
        }

        if (null === $offer) {
            $offer = $this->offers->createNew();
            if ($edition instanceof Edition) {
                $offer->setEdition($edition);
            }

            $offer->addDate(new OfferDate($offer));

            if ($user instanceof User) {
                $offer->setContact([[
                    'name' => $user->getName(),
                    'email' => $user->getEmail(),
                    'phone' => $user->getPhone(),
                ]]);
            }

            $this->denyAccessUnlessGranted('create', $offer);

            $this->entityManager->persist($offer);

            $offerWorkflow->getMarking($offer);

            return $offer;
        }

        if ('admin_offers_copy' === $request->attributes->get('_canonical_route', $request->get('_route'))) {
            $copy = $this->offers->createCopy($offer);

            $this->denyAccessUnlessGranted('view', $offer);
            $this->denyAccessUnlessGranted('create', $copy);

            $this->entityManager->persist($copy);

            $offerWorkflow->getMarking($copy);

            return $copy;
        }

        if ('admin_offers_copy_to_edition' === $request->attributes->get('_canonical_route', $request->get('_route'))) {
            $copy = $this->offers->createCopy($offer, true);

            if (!$edition instanceof Edition) {
                throw new PageNotFoundException('Edition not found');
            }

            $copy->setEdition($edition);

            $this->denyAccessUnlessGranted('view', $offer);
            $this->denyAccessUnlessGranted('create', $copy);

            $this->entityManager->persist($copy);

            $offerWorkflow->getMarking($copy);

            return $copy;
        }

        if ('admin_offers_new_variant' === $request->attributes->get('_canonical_route', $request->get('_route'))) {
            $copy = $this->offers->createVariant($offer);

            if ($edition instanceof Edition) {
                $copy->setEdition($edition);
            }

            $this->denyAccessUnlessGranted('view', $offer);
            $this->denyAccessUnlessGranted('create', $copy);

            $this->entityManager->persist($copy);

            $offerWorkflow->getMarking($copy);

            return $copy;
        }

        $offerWorkflow->getMarking($offer);

        return $offer;
    }
}
