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

use Ferienpass\CoreBundle\ConsentManager\ConsentManager;
use Ferienpass\CoreBundle\ConsentManager\ConsentRequirementChecker;
use Ferienpass\CoreBundle\ConsentManager\ConsentState;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Entity\Participant\ParticipantInterface;
use Ferienpass\CoreBundle\Entity\PostalAddress;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Settings\SystemSettingsManager;

/**
 * Single source of truth that decides WHAT an application needs (participant
 * data, consents, extras, validations) for a given offer + applicant context.
 *
 * Both the signed-in and the anonymous paths in the application form consult
 * this service so they stay aligned — the only difference between them is the
 * data the applicant brings in (logged-in members already have a participant
 * row; anonymous applicants type the data into the form).
 */
class ApplicationFormBuilder
{
    public function __construct(
        private readonly ConsentManager $consentManager,
        private readonly ConsentRequirementChecker $consentRequirementChecker,
        private readonly SystemSettingsManager $systemSettingsManager,
    ) {
    }

    /**
     * Build the spec for an applicant. `$participant` may be null for the
     * anonymous flow before a participant has been created; `$user` is null
     * for anonymous applicants and the to-be-applicant for signed-in members.
     */
    public function build(OfferInterface $offer, ?ParticipantInterface $participant, ?User $user): ApplicationFormSpec
    {
        $participants = $participant instanceof ParticipantInterface ? [$participant] : [];

        $offerConsents = $this->outstanding(
            $this->consentRequirementChecker->getMustSignFirstConsents($offer, $participants, $user)
        );

        if ($participant instanceof ParticipantInterface) {
            $participantConsents = $this->outstanding(array_filter(
                $this->consentManager->forOfferAndParticipants($offer, [$participant], $user),
                static fn (ConsentState $s) => !$s->getConsentForm()->isUserLevel(),
            ));
        } else {
            // Anonymous pre-create: no participant entity yet, but we still need to
            // surface the offer's per-participant consents so the applicant can decide
            // up front. Build pristine states (no signature on file) from the raw
            // ConsentForm collection.
            $participantConsents = [];
            foreach ($this->consentManager->getConsentsForOffer($offer) as $consentForm) {
                if ($consentForm->isUserLevel()) {
                    continue;
                }
                $participantConsents[] = new ConsentState($consentForm, null, null, null);
            }
        }

        return new ApplicationFormSpec(
            offer: $offer,
            needsParticipantData: !$participant instanceof ParticipantInterface,
            offerConsents: $offerConsents,
            participantConsents: $participantConsents,
            extras: $offer->getApplicationExtra() ?? [],
            minAge: $offer->getMinAge(),
            maxAge: $offer->getMaxAge(),
            requiresPostalAddress: $this->systemSettingsManager->isRequirePostalAddress(),
        );
    }

    /**
     * Whether the given postal address satisfies the "minimum complete" rule
     * (street + postal code + city). `country` is allowed to default at persist time.
     */
    public function postalAddressComplete(?PostalAddress $address): bool
    {
        if (!$address instanceof PostalAddress) {
            return false;
        }

        return '' !== (string) $address->getStreet()
            && '' !== (string) $address->getPostalCode()
            && '' !== (string) $address->getCity();
    }

    /**
     * Validate participant data against the spec.
     *
     * Returns a `field path => SHORT per-field translation key` map. The
     * values are the messages rendered next to each affected input (e.g.
     * "Pflichtfeld") — NOT the longer banner copy ("Bitte geben Sie Vorname,
     * Nachname, Geburtsdatum…"), which the caller derives separately. Keeping
     * them short prevents the same paragraph from screaming under every red
     * input.
     *
     * The phone-or-mobile rule fails BOTH fields together since either one
     * would satisfy the constraint — both rings light up red and the banner
     * explains the relationship.
     *
     * @return array<string, string>
     */
    public function validateParticipantData(ParticipantInterface $participant): array
    {
        $errors = [];

        if (!$participant->getFirstname()) {
            $errors['firstname'] = 'applicationForm.anonymous.fieldRequired';
        }

        if (!$participant->getLastname()) {
            $errors['lastname'] = 'applicationForm.anonymous.fieldRequired';
        }

        if (!$participant->getDateOfBirth()) {
            $errors['dateOfBirth'] = 'applicationForm.anonymous.fieldRequired';
        }

        if (!$participant->getEmail()) {
            $errors['email'] = 'applicationForm.anonymous.fieldRequired';
        }

        if (!$participant->getPhone() && !$participant->getMobile()) {
            $errors['phone'] = 'applicationForm.anonymous.fieldPhoneOrMobile';
            $errors['mobile'] = 'applicationForm.anonymous.fieldPhoneOrMobile';
        }

        return $errors;
    }

    /**
     * Whether the offer's consents have all been signed for this participant
     * (i.e. nothing left to render under "Einwilligungen"). Used to decide
     * whether to mark the resulting attendance as `consentOutstanding`.
     * `$participant` may be null (anonymous pre-create) — the pristine,
     * signature-less states are evaluated then.
     */
    public function consentsResolved(OfferInterface $offer, ?ParticipantInterface $participant, ?User $user, array $decisions): bool
    {
        $spec = $this->build($offer, $participant, $user);
        foreach ($spec->offerConsents as $state) {
            if (!$this->decided($decisions, $state)) {
                return false;
            }
        }
        foreach ($spec->participantConsents as $state) {
            if (!$this->decided($decisions, $state)) {
                return false;
            }
        }

        return true;
    }

    /**
     * @param ConsentState[] $states
     *
     * @return ConsentState[]
     */
    private function outstanding(array $states): array
    {
        return array_values(array_filter($states, static fn (ConsentState $s) => $s->isOutstanding()));
    }

    /**
     * @param array<string, '0'|'1'|null> $decisions keyed by ConsentForm id
     */
    private function decided(array $decisions, ConsentState $state): bool
    {
        $key = (string) $state->getConsentForm()->getId();
        $decision = $decisions[$key] ?? null;

        // 'compulsory' must be agreed; 'force_decision' accepts either yes/no.
        return match ($state->getConsentForm()->getCompulsory()) {
            'compulsory' => '1' === $decision,
            'force_decision' => null !== $decision,
            default => true,
        };
    }
}
