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

use Contao\PageModel;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\Query\Expr;
use Doctrine\Persistence\ManagerRegistry;
use Ferienpass\CoreBundle\Entity\Edition;
use Ferienpass\CoreBundle\Entity\User;
use Ferienpass\CoreBundle\Enum\AttendanceStatus;
use Ferienpass\CoreBundle\Enum\OfferStatus;
use Ferienpass\CoreBundle\Facade\DecisionsFacade;

class EditionRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry, private readonly DecisionsFacade $decisionsFacade)
    {
        parent::__construct($registry, Edition::class);
    }

    public function findOneToEdit(): ?Edition
    {
        return $this->findOneWithActiveTask('host_editing_stage');
    }

    /**
     * Find one to show in the frontend on the current page.
     */
    public function findOneToShow(?PageModel $currentPage = null): ?Edition
    {
        $fallback = null;
        foreach ($this->findWithActiveTask('show_offers') as $passEdition) {
            if (!$currentPage instanceof PageModel && null === $fallback) {
                $fallback = $passEdition;
            }
        }

        return $fallback;
    }

    public function findOneWithCurrentHoliday(): ?Edition
    {
        return $this->findOneWithActiveTask('holiday');
    }

    /**
     * @return array<Edition>
     */
    public function findCurrent(): array
    {
        $qb0 = $this->getEntityManager()->createQueryBuilder();
        $qb = $this->createQueryBuilder('edition')
            ->innerJoin(
                'edition.tasks',
                'period',
                Expr\Join::WITH,
                $qb0->expr()->andX(
                    "period.type = 'holiday'",
                    $qb0->expr()->orX(
                        $qb0->expr()->gte('period.periodBegin', 'CURRENT_TIMESTAMP()'),
                        $qb0->expr()->gte('period.periodEnd', ':end')
                    )
                )
            )
            ->orderBy('period.periodEnd', 'DESC')
            ->setParameter('end', new \DateTime('-1 year'), Types::DATETIME_MUTABLE)
            ->getQuery();

        return $qb->getResult();
    }

    /**
     * @return array<int, Edition>
     */
    public function findWithActiveTask(string $taskName): array
    {
        $qb0 = $this->getEntityManager()->createQueryBuilder();
        $qb = $this->createQueryBuilder('edition')
            ->innerJoin(
                'edition.tasks',
                'period',
                Expr\Join::WITH,
                $qb0->expr()->andX(
                    $qb0->expr()->eq('period.type', ':period'),
                    $qb0->expr()->lte('period.periodBegin', 'CURRENT_TIMESTAMP()'),
                    $qb0->expr()->gte('period.periodEnd', 'CURRENT_TIMESTAMP()')
                )
            )
            ->setParameter('period', $taskName)
            ->getQuery();

        return $qb->getResult();
    }

    public function findWithUncommitedAttendances(): ?array
    {
        $qb0 = $this->getEntityManager()->createQueryBuilder();
        $qb1 = $this->decisionsFacade->queryBuilder();
        $qb = $this->createQueryBuilder('edition')
            ->innerJoin('edition.offers', 'offers')
            ->innerJoin(
                'offers.attendances',
                'attendances',
                Expr\Join::WITH,
                $qb0->expr()->in('attendances.id', $qb1->getDQL())
            )
            ->andWhere('edition.archived = 0')
            ->setParameters($qb1->getParameters())
            ->getQuery()
        ;

        return $qb->getResult();
    }

    /**
     * @return array<int, Edition>
     */
    public function findWithActiveAccessCodeStrategies(): array
    {
        $qb0 = $this->getEntityManager()->createQueryBuilder();
        $qb = $this->createQueryBuilder('edition')
            ->innerJoin(
                'edition.tasks',
                'period',
                Expr\Join::WITH,
                $qb0->expr()->andX(
                    $qb0->expr()->eq('period.type', ':period'),
                    $qb0->expr()->isNotNull('period.accessCodeStrategy'),
                    $qb0->expr()->lte('period.periodBegin', 'CURRENT_TIMESTAMP()'),
                    $qb0->expr()->gte('period.periodEnd', 'CURRENT_TIMESTAMP()')
                )
            )
            ->setParameter('period', 'application_system')
            ->getQuery();

        return $qb->getResult();
    }

    public function findOneWithActiveTask(string $taskName): ?Edition
    {
        return $this->findWithActiveTask($taskName)[0] ?? null;
    }

    public function findOneClosestByTask(string $taskName): ?Edition
    {
        $qb0 = $this->getEntityManager()->createQueryBuilder();
        $qb = $this->createQueryBuilder('edition')
            ->innerJoin(
                'edition.tasks',
                'period',
                Expr\Join::WITH,
                $qb0->expr()->andX(
                    $qb0->expr()->eq('period.type', ':period'),
                    $qb0->expr()->gt('period.periodBegin', 'CURRENT_TIMESTAMP()')
                )
            )
            ->setParameter('period', $taskName)
            ->orderBy('period.periodBegin', 'ASC')
            ->getQuery();

        try {
            return $qb->setMaxResults(1)->getOneOrNullResult();
        } catch (NonUniqueResultException) {
            return null;
        }
    }

    public function findUpcoming(string $taskName = 'holiday'): array
    {
        $qb0 = $this->getEntityManager()->createQueryBuilder();
        $qb = $this->createQueryBuilder('edition')
            ->innerJoin(
                'edition.tasks',
                'period',
                Expr\Join::WITH,
                $qb0->expr()->andX(
                    $qb0->expr()->eq('period.type', ':period'),
                    $qb0->expr()->gt('period.periodBegin', 'CURRENT_TIMESTAMP()')
                )
            )
            ->setParameter('period', $taskName)
            ->orderBy('period.periodBegin', 'ASC')
            ->getQuery();

        return $qb->getResult();
    }

    public function findPrevious(string $taskName = 'holiday'): array
    {
        $qb0 = $this->getEntityManager()->createQueryBuilder();
        $qb = $this->createQueryBuilder('edition')
            ->innerJoin(
                'edition.tasks',
                'period',
                Expr\Join::WITH,
                $qb0->expr()->andX(
                    $qb0->expr()->eq('period.type', ':period'),
                    $qb0->expr()->lt('period.periodEnd', 'CURRENT_TIMESTAMP()')
                )
            )
            ->setParameter('period', $taskName)
            ->orderBy('period.periodEnd', 'DESC')
            ->getQuery();

        return $qb->getResult();
    }

    public function findWithFinalizedOffers(bool $showCompletedEditions = false): array
    {
        $qb = $this->createQueryBuilder('edition');

        $qb
            ->innerJoin('edition.offers', 'offer')
            ->andWhere(\sprintf("JSON_CONTAINS_PATH(offer.status, 'one', '$.%s') = 1", OfferStatus::FINALIZED))
            ->andWhere(\sprintf("JSON_CONTAINS_PATH(offer.status, 'one', '$.%s') = 0", OfferStatus::COMPLETED))
        ;

        if (!$showCompletedEditions) {
            $qb->andWhere('edition.archived = 0');
        }

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

    public function findWithWaitingAttendances(): array
    {
        $qb = $this->createQueryBuilder('edition');

        $qb
            ->addSelect('SUM(CASE WHEN attendance.status = :status_confirmed THEN 1 ELSE 0 END) AS HIDDEN countAttendancesConfirmed')
            ->addSelect('SUM(CASE WHEN attendance.status = :status_waitlisted THEN 1 ELSE 0 END) AS HIDDEN countAttendancesWaitlisted')
            ->addSelect('SUM(CASE WHEN attendance.status = :status_waiting THEN 1 ELSE 0 END) AS HIDDEN countAttendancesWaiting')
            ->addSelect('MAX(offer.maxParticipants) AS HIDDEN maxParticipants')
            ->innerJoin('edition.offers', 'offer')
            ->andWhere(\sprintf("JSON_CONTAINS_PATH(offer.status, 'one', '$.%s') = 0", OfferStatus::CANCELLED))
            ->innerJoin('offer.attendances', 'attendance')
            ->andWhere('edition.archived = 0')
            ->innerJoin('offer.dates', 'date')
            ->andWhere($qb->expr()->orX($qb->expr()->isNotNull('date.begin'), 'date.begin > CURRENT_TIMESTAMP()'))
            ->having($qb->expr()->orX(
                // WAITING attendances
                'countAttendancesWaiting > 0',
                // WAITLISTED attendances despite spare spots
                $qb->expr()->andX(
                    'maxParticipants > 0',
                    'countAttendancesConfirmed < maxParticipants',
                    'countAttendancesWaitlisted > 0'
                )
            ))
            ->groupBy('offer.id')
            ->addGroupBy('edition.id')
            ->setParameter('status_confirmed', AttendanceStatus::CONFIRMED)
            ->setParameter('status_waitlisted', AttendanceStatus::WAITLISTED)
            ->setParameter('status_waiting', AttendanceStatus::WAITING)
        ;

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

    /**
     * Non-archived editions that require a host-agreement signature and that the
     * given user is a host of. Used to gate tools without an edition context
     * (e.g. Rundmail): the user is blocked while any of these is unsigned.
     *
     * @return array<Edition>
     */
    public function findActiveHostAgreementEditionsForUser(User $user): array
    {
        $hosts = $user->getHosts();
        if ($hosts->isEmpty()) {
            return [];
        }

        return $this->createQueryBuilder('edition')
            ->innerJoin('edition.hosts', 'host')
            ->innerJoin('edition.consentForms', 'cf')
            ->andWhere('host IN (:hosts)')
            ->andWhere('edition.archived = 0')
            ->andWhere("cf.subject = 'host'")
            ->andWhere('cf.consentText IS NOT NULL')
            ->setParameter('hosts', $hosts->getValues())
            ->getQuery()
            ->getResult()
        ;
    }

    /**
     * Editions carrying a host declaration, newest first — the source list for
     * the editor's "copy from a previous season" import.
     *
     * @return array<Edition>
     */
    public function findWithHostAgreement(): array
    {
        return $this->createQueryBuilder('edition')
            ->innerJoin('edition.consentForms', 'cf')
            ->andWhere("cf.subject = 'host'")
            ->andWhere('cf.consentText IS NOT NULL')
            ->orderBy('edition.createdAt', 'DESC')
            ->getQuery()
            ->getResult()
        ;
    }
}
