<?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\Components;

use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\CoreBundle\ConsentManager\ConsentManager;
use Ferienpass\CoreBundle\ConsentManager\ConsentState;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\Consent;
use Ferienpass\CoreBundle\Entity\ConsentForm;
use Ferienpass\CoreBundle\Entity\Participant\ParticipantInterface;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Enum\AttendanceStatus;
use Ferienpass\CoreBundle\Enum\AttendanceTransition;
use Ferienpass\CoreBundle\Repository\ConsentRepository;
use Ferienpass\CoreBundle\Workflow\CompulsoryWithdrawalContext;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Workflow\WorkflowInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveArg;
use Symfony\UX\LiveComponent\DefaultActionTrait;
use Symfony\UX\TwigComponent\Attribute\ExposeInTemplate;

#[AsLiveComponent(template: '@Contao/components/consents.html.twig', route: 'live_component_cms')]
class Consents extends AbstractController
{
    use DefaultActionTrait;

    public function __construct(
        private readonly ConsentRepository $consents,
        private readonly ConsentManager $consentManager,
        private readonly EntityManagerInterface $entityManager,
        private readonly Security $security,
        private readonly WorkflowInterface $attendanceStateMachine,
        private readonly CompulsoryWithdrawalContext $compulsoryWithdrawal,
    ) {
    }

    /**
     * All consent signatures of the user, one state per (form, subject) pair,
     * grouped by edition. Archived editions are hidden.
     *
     * @return array<array{edition: \Ferienpass\CoreBundle\Entity\Edition|null, states: ConsentState[]}>
     */
    #[ExposeInTemplate]
    public function consentGroups(): array
    {
        $user = $this->getUser();
        if (!$user instanceof User) {
            return [];
        }

        // Only the relevant signature per consent form and subject: a re-signed
        // or revoked consent leaves older rows behind that would show up twice.
        $bySubject = [];
        foreach ($this->consents->findAllForUser($user) as $signature) {
            $form = $signature->getConsentForm();
            $key = ($form?->getId() ?? 'legacy-'.($signature->getHash() ?? $signature->getId()))
                .'/'.($signature->getParticipant()?->getId() ?? 'account');
            $bySubject[$key][] = $signature;
        }

        $groups = [];
        foreach ($bySubject as $signatures) {
            $signature = $this->newestValid($signatures);
            $state = new ConsentState($signature->getConsentForm(), $signature, $signature->getParticipant(), $user);

            $edition = $state->getEdition();
            if ($edition?->isArchived()) {
                continue;
            }

            $groupKey = $edition?->getId() ?? '';
            $groups[$groupKey] ??= ['edition' => $edition, 'states' => []];
            $groups[$groupKey]['states'][] = $state;
        }

        // Within an edition, account-level consents come first
        foreach ($groups as &$group) {
            usort($group['states'], static fn (ConsentState $a, ConsentState $b) => (null !== $a->getParticipant()) <=> (null !== $b->getParticipant()));
        }

        // Newest edition first, consents without edition last
        uasort($groups, static function (array $a, array $b) {
            $aBegin = $a['edition']?->getHoliday()?->getPeriodBegin();
            $bBegin = $b['edition']?->getHoliday()?->getPeriodBegin();

            return [null === $a['edition'], $bBegin] <=> [null === $b['edition'], $aBegin];
        });

        return array_values($groups);
    }

    /**
     * Change a decision (re-grant, restore, or flip a force-decision). The
     * signature reference identifies the consent form and subject.
     */
    #[LiveAction]
    public function decide(#[LiveArg] int $signature, #[LiveArg] string $decision): void
    {
        $consent = $this->findOwnSignature($signature);
        $form = $consent?->getConsentForm();
        if (null === $form) {
            return;
        }

        if ('1' === $decision) {
            $this->consentManager->sign($form, $consent->getUser(), $consent->getParticipant());
        } else {
            $this->consentManager->decline($form, $consent->getUser(), $consent->getParticipant());
        }
    }

    /**
     * Withdraw a consent with effect for the future. For force-decision
     * consents this resets the decision to "outstanding".
     */
    #[LiveAction]
    public function revoke(#[LiveArg] int $signature): void
    {
        $consent = $this->findOwnSignature($signature);
        if (null === $consent) {
            return;
        }

        $consent->revoke();

        // A compulsory consent is a precondition for the application. Once it
        // is withdrawn we may no longer process the affected attendances, so
        // they are withdrawn alongside it. Withdrawal takes effect for the
        // FUTURE only: the workflow guard keeps already-started offers
        // untouched, mirroring the "future effect" rule of the consent itself.
        if ('compulsory' === $consent->getConsentForm()?->getCompulsory()) {
            $this->withdrawAttendancesGatedBy($consent);
        }

        $this->entityManager->flush();
    }

    /**
     * Withdraw every still-active attendance whose participation is gated by
     * the just-revoked compulsory consent.
     */
    private function withdrawAttendancesGatedBy(Consent $consent): void
    {
        $form = $consent->getConsentForm();
        $user = $consent->getUser();
        if (null === $form || !$user instanceof User) {
            return;
        }

        // A per-participant consent only affects its own child; an account
        // consent affects every participant of the user.
        $participant = $consent->getParticipant();
        $participants = $participant instanceof ParticipantInterface
            ? [$participant]
            : $user->getParticipants()->toArray();

        // Flag the whole cascade as compliance-driven so the guard waives the
        // organiser's no-withdraw-after-deadline/-grace convenience rules. It
        // still blocks cancelled / already-started offers, keeping withdrawal
        // future-effective. Both can() and apply() must run inside this scope.
        $this->compulsoryWithdrawal->run(function () use ($participants, $form, $participant): void {
            foreach ($participants as $p) {
                foreach ($p->getAttendances() as $attendance) {
                    if (AttendanceStatus::WITHDRAWN === $attendance->getStatus()) {
                        continue;
                    }

                    if (!$this->consentGatesAttendance($form, $participant, $attendance)) {
                        continue;
                    }

                    // Already-started offers stay (the guard blocks them) —
                    // withdrawal is future-effective, like the consent itself.
                    if (!$this->attendanceStateMachine->can($attendance, AttendanceTransition::WITHDRAW)) {
                        continue;
                    }

                    $this->attendanceStateMachine->apply($attendance, AttendanceTransition::WITHDRAW, ['notify' => false]);
                }
            }
        });
    }

    /**
     * Whether participating in {@see $attendance}'s offer requires {@see $form}.
     */
    private function consentGatesAttendance(ConsentForm $form, ?ParticipantInterface $participant, Attendance $attendance): bool
    {
        // A per-participant consent only gates the matching child's attendance.
        if ($form->isPerParticipant() && $attendance->getParticipant() !== $participant) {
            return false;
        }

        // A global account consent (no offer, no edition) gates every application.
        if (null === $form->getOffer() && null === $form->getEdition()) {
            return true;
        }

        // Otherwise the offer must actually require this consent form — this
        // covers both offer-scoped and edition-scoped consents.
        return \in_array($form, $this->consentManager->getConsentsForOffer($attendance->getOffer()), true);
    }

    private function findOwnSignature(int $id): ?Consent
    {
        $user = $this->getUser();
        if (!$user instanceof User) {
            return null;
        }

        $signature = $this->consents->find($id);
        if (null === $signature) {
            return null;
        }

        if ($signature->getUser() !== $user && $signature->getParticipant()?->getUser() !== $user) {
            return null;
        }

        return $signature;
    }

    /**
     * @param Consent[] $signatures
     */
    private function newestValid(array $signatures): Consent
    {
        usort($signatures, static function (Consent $a, Consent $b) {
            $text = $a->getConsentForm()?->getText();
            if (($aValid = $a->isValid($text)) !== ($bValid = $b->isValid($text))) {
                return $aValid ? -1 : 1;
            }

            return $b->getCreatedAt() <=> $a->getCreatedAt();
        });

        return $signatures[0];
    }
}
