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

use Contao\CoreBundle\Cron\Cron;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCronJob;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\QueryBuilder;
use Ferienpass\CoreBundle\Entity\Offer\OfferInterface;
use Ferienpass\CoreBundle\Enum\OfferStatus;
use Ferienpass\CoreBundle\Message\SendParticipantList;
use Ferienpass\CoreBundle\Repository\OfferRepositoryInterface;
use Symfony\Component\Messenger\MessageBusInterface;

/**
 * Mails the participant and attendance lists to the configured hosts shortly
 * before the event: at the later of the application deadline or 24 hours before
 * the first date begins.
 */
#[AsCronJob('hourly')]
class SendParticipantLists
{
    public function __construct(private readonly OfferRepositoryInterface $offers, private readonly MessageBusInterface $commandBus)
    {
    }

    public function __invoke(string $scope): void
    {
        if (Cron::SCOPE_WEB === $scope) {
            return;
        }

        $published = OfferStatus::PUBLISHED;
        $cancelled = OfferStatus::CANCELLED;

        /** @var QueryBuilder $qb */
        $qb = $this->offers->createQueryBuilder('o');

        /** @var Collection<OfferInterface> $offers */
        $offers = $qb
            // Only offers that have at least one opted-in recipient host
            ->innerJoin('o.participantListRecipients', 'r')
            ->innerJoin('o.dates', 'd')
            ->groupBy('o.id')

            // LEFT JOIN event logs, because we want to filter out offers that were notified already.
            ->leftJoin('o.messengerLogs', 'm', Join::WITH, 'm.message = :message')
            ->setParameter('message', SendParticipantList::class)
            ->andWhere('m IS NULL')

            // Defensive: only offers with online application
            ->andWhere('o.onlineApplication = 1')

            // Must be published and not cancelled
            ->andWhere("JSON_CONTAINS_PATH(o.status, 'one', '$.$published') = 1")
            ->andWhere("JSON_CONTAINS_PATH(o.status, 'one', '$.$cancelled') = 0")

            // The application deadline must be past (or unset) …
            ->andWhere($qb->expr()->orX('o.applicationDeadline IS NULL', 'o.applicationDeadline <= CURRENT_TIMESTAMP()'))

            // … and we must be within 24 hours of the first date (i.e. now >= begin - 24h) …
            ->andHaving('MIN(d.begin) <= :threshold')
            ->setParameter('threshold', new \DateTimeImmutable('+24 hours'), Types::DATETIME_IMMUTABLE)

            // … while the event is not over yet (COALESCE guards dates without an explicit end).
            ->andHaving('MAX(COALESCE(d.end, d.begin)) > CURRENT_TIMESTAMP()')

            ->getQuery()
            ->execute()
        ;

        foreach ($offers as $offer) {
            $this->commandBus->dispatch(new SendParticipantList((string) $offer->getUuid()));
        }
    }
}
