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

use Contao\CoreBundle\Exception\PageNotFoundException;
use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\CmsBundle\Controller\AbstractController;
use Ferienpass\CmsBundle\Fragment\FragmentReference;
use Ferienpass\CmsBundle\Security\PasskeyRequirement;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Message\AccountActivated;
use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\UriSigner;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Routing\Requirement\Requirement;

class RegistrationController extends AbstractController
{
    public function __construct(private readonly UriSigner $uriSigner, private readonly Security $security, private readonly MessageBusInterface $eventBus, private readonly PasskeyRequirement $passkeyRequirement)
    {
    }

    #[Route('/registrierung/{uuid}', name: 'cms_registration_confirm', requirements: ['uuid' => Requirement::UUID])]
    public function confirm(#[MapEntity(mapping: ['uuid' => 'uuid'])] User $user, Request $request, EntityManagerInterface $entityManager): Response
    {
        $this->initializeContaoFramework();

        $currentUser = $this->getUser();
        $isCurrentUser = $currentUser instanceof User && $currentUser->getId() === $user->getId();

        $validLink = $this->isValidActivationLink($request);

        // The activation itself only happens on POST. Mail clients and security
        // scanners routinely PREFETCH links — but with GET, and without running
        // JS or submitting forms. Keeping the state change behind POST means a
        // prefetch can never enable the account (and never trigger the "account
        // activated" mail); the signed GET below just renders a page that POSTs
        // back. We still only flip the flag + announce activation the first
        // time, and the valid link logs the user in even if the account is
        // already enabled (e.g. the link is opened twice).
        if ($request->isMethod('POST') && $validLink && !$isCurrentUser) {
            if ($user->isDisabled()) {
                $user->setDisabled(false);

                $entityManager->flush();

                $this->eventBus->dispatch(new AccountActivated((string) $user->getUuid()));
            }

            $this->security->login($user, 'json_login', 'contao_frontend');

            return $this->redirectToRoute($this->nextOnboardingStep($user));
        }

        // The account is active and the matching user is logged in: continue onboarding.
        if (!$user->isDisabled() && $isCurrentUser) {
            return $this->redirectToRoute($this->nextOnboardingStep($user));
        }

        // Signed activation link opened in a browser (GET): render a page that
        // immediately POSTs back to confirm — with a no-JS fallback button — so
        // the account is enabled by a real user interaction, not a prefetch.
        if ($validLink && !$isCurrentUser) {
            return $this->markPageAsPrivate(
                $this->createPageBuilder()
                    ->addFragment('main', new FragmentReference('ferienpass.fragment.registration_activate', ['actionUrl' => $request->getRequestUri()]))
                    ->getResponse()
            );
        }

        // "Check your e-mail" page: freshly registered, not logged in, no link.
        // Marked private (like the other branches) so this route carries its own
        // cache policy and doesn't depend on a central fos_http_cache carve-out.
        if ($user->isDisabled() && !$isCurrentUser) {
            return $this->markPageAsPrivate(
                $this->createPageBuilder()
                    ->addFragment('main', new FragmentReference('ferienpass.fragment.registration_confirm'))
                    ->getResponse()
            );
        }

        throw new PageNotFoundException();
    }

    #[Route('/registrierung/willkommen', name: 'cms_registration_welcome')]
    public function welcome(): Response
    {
        $this->initializeContaoFramework();

        $this->checkToken();

        // A passkey-only member without a credential must set one up first.
        $user = $this->getUser();
        if ($user instanceof User && $this->passkeyRequirement->needsPasskey($user)) {
            return $this->redirectToRoute('cms_registration_passkey');
        }

        return $this->markPageAsPrivate(
            $this->createPageBuilder()
                ->addFragment('main', new FragmentReference('ferienpass.fragment.registration_welcome'))
                ->getResponse()
        );
    }

    #[Route('/registrierung/passkey', name: 'cms_registration_passkey')]
    public function setupPasskey(): Response
    {
        $this->initializeContaoFramework();

        $this->checkToken();

        $user = $this->getUser();
        if (!$user instanceof User || !$this->passkeyRequirement->needsPasskey($user)) {
            return $this->redirectToRoute('cms_registration_welcome');
        }

        return $this->markPageAsPrivate(
            $this->createPageBuilder()
                ->addFragment('main', new FragmentReference('ferienpass.fragment.registration_passkey'))
                ->getResponse()
        );
    }

    #[Route('/email-abgemeldet', name: 'cms_blocked_email')]
    public function blockedEmail(Request $request): Response
    {
        $this->initializeContaoFramework();

        if (!$this->uriSigner->check($request->getUri())) {
            throw new PageNotFoundException();
        }

        if (!$request->query->has('valid') || new \DateTimeImmutable($request->query->get('valid')) < new \DateTimeImmutable()) {
            throw new PageNotFoundException();
        }

        return $this->createPageBuilder()
            ->addFragment('main', new FragmentReference('ferienpass.fragment.account_unsubscribed'))
            ->getResponse()
        ;
    }

    #[Route('/account-geloescht', name: 'cms_account_deleted')]
    public function accountDeleted(): Response
    {
        $this->initializeContaoFramework();

        return $this->createPageBuilder()
            ->addFragment('main', new FragmentReference('ferienpass.fragment.account_deleted'))
            ->getResponse()
        ;
    }

    /**
     * After registration the onboarding must not be skippable for a passkey-only
     * account that has no credential yet — otherwise the member ends up unable
     * to log in. Send them to the passkey setup first.
     */
    private function nextOnboardingStep(User $user): string
    {
        return $this->passkeyRequirement->needsPasskey($user) ? 'cms_registration_passkey' : 'cms_registration_welcome';
    }

    private function isValidActivationLink(Request $request): bool
    {
        if (!$this->uriSigner->checkRequest($request)) {
            return false;
        }

        $valid = $request->query->get('valid');
        if (!\is_string($valid) || '' === $valid) {
            return false;
        }

        try {
            return new \DateTimeImmutable($valid) >= new \DateTimeImmutable();
        } catch (\DateMalformedStringException) {
            return false;
        }
    }
}
