<?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 Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Ferienpass\CoreBundle\Entity\Attendance;
use Ferienpass\CoreBundle\Entity\Participant\ParticipantInterface;
use Ferienpass\CoreBundle\Entity\Payment;
use Ferienpass\CoreBundle\Enum\PaymentStatus;
use Symfony\Bridge\Doctrine\Types\UuidType;
use Symfony\Component\Uid\Uuid;

class PaymentRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Payment::class);
    }

    public function findPaymentByAttendances(array $attendances, ?int $totalAmount = null, bool $onlyUnpaid = true): ?Payment
    {
        $qb0 = $this->getEntityManager()->createQueryBuilder();
        $qb = $this->createQueryBuilder('payment')
            ->innerJoin('payment.items', 'item')
            ->innerJoin('item.attendance', 'attendance')
            ->groupBy('payment.id')
        ;

        // https://stackoverflow.com/a/14113307
        // DQL does not allow conditional expressions (NOT IN, =) directly inside an
        // aggregate like MySQL does, so wrap them in CASE WHEN to yield 1/0.
        $qb->andHaving('SUM(CASE WHEN attendance.id NOT IN (:attendanceIds) THEN 1 ELSE 0 END) = 0');
        $qb->setParameter('attendanceIds', array_map(static fn (Attendance $a) => $a->getId(), $attendances));

        /** @var Attendance $attendance */
        foreach ($attendances as $i => $attendance) {
            $qb->andHaving("SUM(CASE WHEN attendance.id = :att{$i} THEN 1 ELSE 0 END) = 1");
            $qb->setParameter('att'.$i, $attendance->getId());
        }

        if (null !== $totalAmount) {
            $qb->andWhere('payment.totalAmount = :totalAmount');
            $qb->setParameter('totalAmount', $totalAmount);
        }

        if ($onlyUnpaid) {
            $qb->andWhere($qb0->expr()->neq('payment.status', ':status_paid'));
            $qb->setParameter('status_paid', PaymentStatus::PAID);
        }

        return $qb->setMaxResults(1)->getQuery()->getOneOrNullResult();
    }

    public function findPayableByUuid(string $uuid): ?Payment
    {
        try {
            $uuid = Uuid::fromRfc4122($uuid);
        } catch (\InvalidArgumentException) {
            return null;
        }

        $qb0 = $this->getEntityManager()->createQueryBuilder();
        $qb = $this->createQueryBuilder('payment')
            ->leftJoin('payment.items', 'item')
            ->leftJoin('item.attendance', 'attendance')
            ->andWhere($qb0->expr()->eq('payment.uuid', ':uuid'))
            ->andWhere($qb0->expr()->neq('payment.status', ':status_paid'))
            ->setParameter('uuid', $uuid, UuidType::NAME)
            ->setParameter('status_paid', PaymentStatus::PAID)
            ->setMaxResults(1)
            ->getQuery()
        ;

        return $qb->getOneOrNullResult();
    }

    /**
     * @return array<Payment>
     */
    public function findByParticipant(ParticipantInterface $participant): array
    {
        return $this->createQueryBuilder('pay')
            ->innerJoin('pay.items', 'i')
            ->innerJoin('i.attendance', 'a')
            ->innerJoin('a.participant', 'p')
            ->where('p.id = :id')
            ->setParameter('id', $participant->getId())
            ->getQuery()
            ->getResult()
        ;
    }
}
