<?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\AdminBundle\Service;

use Doctrine\ORM\QueryBuilder;
use Ferienpass\CoreBundle\Entity\Host;
use Ferienpass\CoreBundle\Entity\Participant\ParticipantInterface;
use Ferienpass\CoreBundle\Enum\AttendanceStatus;
use Ferienpass\CoreBundle\Facade\PaymentsFacade;
use Ferienpass\CoreBundle\Repository\ParticipantRepositoryInterface;

/**
 * Resolves who a Rundmail to "Teilnehmende" actually reaches.
 *
 * The attendance status is not an optional refinement of that question but the
 * whole of it: "die Teilnehmenden des Angebots" means the children who got a
 * seat, never the waiting list and never the families that were turned down.
 * An empty selection therefore resolves to nobody — it used to drop the status
 * condition altogether, which silently mailed every applicant of the offer.
 * Unknown status strings (a hand-edited URL, a stale bookmark) are dropped for
 * the same reason: they must not widen the audience.
 */
final class MailingRecipients
{
    /**
     * Every attendance status a Rundmail can be aimed at, in the order the filter
     * offers them: seat taken, then undecided, then no seat. Kept complete against
     * {@see AttendanceStatus} by MailingRecipientsTest — a status missing here can
     * neither be reached nor be excluded on purpose.
     *
     * @var list<string>
     */
    public const array STATUSES = [
        AttendanceStatus::CONFIRMED,
        AttendanceStatus::PARTICIPATED,
        AttendanceStatus::NO_SHOW,
        AttendanceStatus::WAITING,
        AttendanceStatus::WAITLISTED,
        AttendanceStatus::EMAIL_UNCONFIRMED,
        AttendanceStatus::CONSENTS_OUTSTANDING,
        AttendanceStatus::UNFULFILLED,
        AttendanceStatus::REJECTED,
        AttendanceStatus::WITHDRAWN,
    ];

    /**
     * The statuses that mean "has a seat". Everything else is worth warning about
     * before the mail goes out.
     *
     * @var list<string>
     */
    public const array ALLOCATED = [
        AttendanceStatus::CONFIRMED,
        AttendanceStatus::PARTICIPATED,
        AttendanceStatus::NO_SHOW,
    ];

    /**
     * What the filter starts out with: the seat holders plus the applicants the
     * lottery could not place, because a Rundmail about an offer usually has to
     * reach both. That second group has no seat, so the selection deliberately
     * trips the "ohne Platz" warning — the audience stays wide but never silent.
     *
     * @var list<string>
     */
    public const array DEFAULT_STATUSES = [AttendanceStatus::CONFIRMED, AttendanceStatus::UNFULFILLED];

    public function __construct(private readonly ParticipantRepositoryInterface $participantRepository)
    {
    }

    /**
     * The selected statuses that do not mean "has a seat".
     *
     * @param list<string> $statuses
     *
     * @return list<string>
     */
    public static function withoutSeat(array $statuses): array
    {
        return array_values(array_diff(self::sanitize($statuses), self::ALLOCATED));
    }

    /**
     * @param list<string>        $statuses
     * @param list<int|string>    $offerIds
     * @param list<int|string>    $editionIds
     * @param iterable<Host>|null $hosts      null lifts the host restriction (admins); an empty list restricts to nothing
     *
     * @return list<ParticipantInterface>
     */
    public function resolve(array $statuses, array $offerIds = [], array $editionIds = [], ?iterable $hosts = null, bool $onlyUnacknowledged = false, bool $onlyUnpaid = false): array
    {
        $statuses = self::sanitize($statuses);
        if ([] === $statuses) {
            return [];
        }

        /** @var QueryBuilder $qb */
        $qb = $this->participantRepository->createQueryBuilder('participant');
        $qb
            ->innerJoin('participant.attendances', 'attendance')
            ->innerJoin('attendance.offer', 'offer')
            ->leftJoin('offer.edition', 'edition')
            ->andWhere('attendance.status IN (:status)')
            ->setParameter('status', $statuses)
        ;

        if (null !== $hosts) {
            $qb->innerJoin('offer.hosts', 'hosts')
                ->andWhere('hosts IN (:hosts)')
                ->setParameter('hosts', $hosts instanceof \Traversable ? iterator_to_array($hosts) : $hosts)
            ;
        }

        if ($offerIds) {
            $qb->andWhere('offer IN (:offers)')->setParameter('offers', $offerIds);
        }

        if ($editionIds) {
            $qb->andWhere('edition IN (:editions)')->setParameter('editions', $editionIds);
        }

        if ($onlyUnacknowledged) {
            PaymentsFacade::addWhereLogic($qb);

            if ($onlyUnpaid) {
                $qb->andWhere('offer.fee > 0');
            }
        }

        return $qb->getQuery()->getResult();
    }

    /**
     * @param list<string> $statuses
     *
     * @return list<string>
     */
    private static function sanitize(array $statuses): array
    {
        return array_values(array_intersect(self::STATUSES, $statuses));
    }
}
