<?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 Ferienpass\CmsBundle\Controller\AbstractController;
use Ferienpass\CmsBundle\Fragment\FragmentReference;
use Ferienpass\CoreBundle\ConsentManager\ConsentManager;
use Ferienpass\CoreBundle\ConsentManager\ConsentState;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Facade\AttendanceFacade;
use Ferienpass\CoreBundle\Repository\AttendanceRepository;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;

/**
 * The shared consent-signing page for an attendance with outstanding consents.
 * The token is handed to the applicant (e.g. a minor who postponed the consents
 * to their legal guardian) or shared by an admin; whoever holds it signs the
 * offer's consents in the name of the legal guardian. Once signed, a held
 * application ({@see AttendanceStatus::CONSENTS_OUTSTANDING}) is released into
 * the normal allocation.
 */
class ConsentLinkController extends AbstractController
{
    #[Route('/einwilligung/{token}', name: 'cms_consent_link', methods: ['GET', 'POST'])]
    public function __invoke(string $token, Request $request, AttendanceRepository $attendanceRepository, ConsentManager $consentManager, AttendanceFacade $attendanceFacade): Response
    {
        $this->initializeContaoFramework();

        $attendance = $attendanceRepository->findOneBy(['consentToken' => $token]);
        if (!$attendance) {
            throw $this->createNotFoundException();
        }

        $error = null;
        if ($request->isMethod('POST')) {
            $error = $this->sign($request, $attendance, $consentManager, $attendanceFacade);
            if (null === $error) {
                // PRG — a reload must not re-submit the signature.
                return $this->redirectToRoute('cms_consent_link', ['token' => $token, 'signed' => 1]);
            }
        }

        $response = $this->createPageBuilder()
            ->addFragment('main', new FragmentReference('ferienpass.fragment.consent_sign', [
                'token' => $token,
                'error' => $error,
                'signed' => $request->query->getBoolean('signed'),
            ]))
            ->getResponse()
        ;

        // The consent token travels in the URL and acts as the sole bearer
        // credential. Prevent it from leaking to third parties via the
        // Referer header when the consent page links out, and keep the
        // personal data on this page out of shared caches.
        $response->headers->set('Referrer-Policy', 'no-referrer');
        $response->setPrivate();

        return $response;
    }

    /**
     * Validate and persist the guardian's decisions. Returns a translation key
     * on validation failure, null on success.
     */
    private function sign(Request $request, Attendance $attendance, ConsentManager $consentManager, AttendanceFacade $attendanceFacade): ?string
    {
        $participant = $attendance->getParticipant();
        if (null === $participant) {
            return 'consentSign.error.incomplete';
        }

        $signedBy = trim((string) $request->request->get('signedBy'));
        if ('' === $signedBy) {
            return 'consentSign.error.nameRequired';
        }

        $submitted = $request->request->all('decisions');
        $states = $this->participantStates($attendance, $consentManager);

        // Validate first — nothing is persisted unless the set is complete.
        $decisions = [];
        foreach ($states as $state) {
            $form = $state->getConsentForm();
            $decision = $submitted[(string) $form->getId()] ?? null;
            $decision = \in_array($decision, ['1', '0'], true) ? $decision : null;

            // Unchecked checkboxes are not submitted: opt-in/opt-out default to "no".
            $decision ??= match ($form->getCompulsory()) {
                'opt-in', 'opt-out' => '0',
                default => null,
            };

            if (null === $decision || ('compulsory' === $form->getCompulsory() && '1' !== $decision)) {
                return 'consentSign.error.incomplete';
            }

            $decisions[] = [$form, $decision];
        }

        foreach ($decisions as [$form, $decision]) {
            $state = '1' === $decision
                ? $consentManager->sign($form, null, $participant)
                : $consentManager->decline($form, null, $participant);

            $state->getSignature()?->setSignedBy($signedBy);
        }

        // Releases a held application into the normal allocation (and flushes
        // the signee names set above).
        $attendanceFacade->confirmConsents($attendance);

        return null;
    }

    /**
     * Every consent the offer requires for this participant — both account-level
     * (step-1) and per-participant. The guardian signs them all via the link, so
     * none are filtered out; account-level ones are recorded against the
     * participant (user=null) and rebind to the account on later registration.
     *
     * @return ConsentState[]
     */
    private function participantStates(Attendance $attendance, ConsentManager $consentManager): array
    {
        return array_values($consentManager->forOfferAndParticipants($attendance->getOffer(), [$attendance->getParticipant()]));
    }
}
