<?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\CoreBundle\ConsentManager;

use Doctrine\ORM\EntityManagerInterface;
use Ferienpass\CoreBundle\Entity\Consent;
use Ferienpass\CoreBundle\Entity\ConsentForm;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\Participant\ParticipantInterface;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Repository\ConsentFormRepository;
use Ferienpass\CoreBundle\Repository\ConsentRepository;

class ConsentManager
{
    public function __construct(
        private ConsentFormRepository $consentFormRepository,
        private ConsentRepository $consentRepository,
        private EntityManagerInterface $entityManager
    ) {
    }

    public function findState(ConsentForm $consentForm, User $user, ?ParticipantInterface $participant = null): ConsentState
    {
        $states = $this->forFormsAndParticipants([$consentForm], array_filter([$participant]), $user);

        return $states[0];
    }

    public function sign(ConsentForm $consentForm, ?User $user, ?ParticipantInterface $participant = null): ConsentState
    {
        return $this->updateDecision($consentForm, $user, $participant, 1);
    }

    public function decline(ConsentForm $consentForm, ?User $user, ?ParticipantInterface $participant = null): ConsentState
    {
        return $this->updateDecision($consentForm, $user, $participant, 0);
    }

    public function getConsentsForOffer(OfferInterface $offer): array
    {
        return $this->consentFormRepository->findForOffer($offer);
    }

    /**
     * @param ParticipantInterface[] $participants
     *
     * @return ConsentState[]
     */
    public function forOfferAndParticipants(OfferInterface $offer, array $participants, ?User $user = null): array
    {
        return $this->forFormsAndParticipants($this->getConsentsForOffer($offer), $participants, $user);
    }

    /**
     * @param ConsentForm[]          $consentForms
     * @param ParticipantInterface[] $participants
     *
     * @return ConsentState[]
     */
    public function forFormsAndParticipants(array $consentForms, array $participants, ?User $user = null): array
    {
        $user ??= (!empty($participants) ? $participants[0]->getUser() : null);

        $signatures = $this->consentRepository->findSignatures($consentForms, $user, $participants);

        $states = [];
        foreach ($consentForms as $form) {
            // CARDINALITY is driven by the subject, NOT by gate-vs-inline
            // placement: account → one state; participant → one state per
            // participant (even for edition-scoped forms shown in the gate).
            $perAccount = !$form->isPerParticipant();
            $subjects = $perAccount ? [null] : ($participants ?: [null]);

            foreach ($subjects as $subject) {
                $targetUser = $perAccount ? $user : ($subject?->getUser() ?? $user);

                $candidates = array_filter($signatures, fn (Consent $sig) => $this->signatureMatches($sig, $form, $subject, $perAccount));

                $best = $this->findBestSignature($candidates, $form->getText(), $targetUser);

                $states[] = new ConsentState($form, $best, $perAccount ? null : $subject, $targetUser);
            }
        }

        return $states;
    }

    private function signatureMatches(Consent $sig, ConsentForm $form, ?ParticipantInterface $subject, bool $perAccount): bool
    {
        $matchByForm = $sig->getConsentForm() === $form;
        $matchByHash = $sig->getHash() && $sig->getHash() === $form->getConsentText()?->getHash();

        if (!$matchByForm && !$matchByHash) {
            return false;
        }

        return $perAccount || $sig->getParticipant() === $subject;
    }

    private function updateDecision(ConsentForm $consentForm, ?User $user, ?ParticipantInterface $participant, int $decision): ConsentState
    {
        /** @var Consent|null $signature */
        $signature = $this->consentRepository->findOneBy([
            'consentForm' => $consentForm,
            'participant' => $participant,
            'user' => $user,
        ], ['createdAt' => 'DESC']);

        if ($signature && !$signature->isValid($consentForm->getText())) {
            $signature = null;
        }

        if ($signature) {
            $signature->setDecision($decision);
        } else {
            $signature = new Consent($user, participant: $participant, consentForm: $consentForm, decision: $decision);
            $this->entityManager->persist($signature);
        }

        $this->entityManager->flush();

        return new ConsentState($consentForm, $signature, $participant, $user);
    }

    /**
     * @param Consent[] $signatures
     */
    private function findBestSignature(array $signatures, ?string $consentText, ?User $user): ?Consent
    {
        if (null !== $user) {
            $signatures = array_filter($signatures, static fn (Consent $s) => $s->getUser() === $user);
        }

        if ([] === $signatures) {
            return null;
        }

        usort($signatures, static function (Consent $a, Consent $b) use ($consentText) {
            if (($aValid = $a->isValid($consentText)) !== ($bValid = $b->isValid($consentText))) {
                return $aValid ? -1 : 1;
            }

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

        return $signatures[0];
    }
}
