<?php

/**
 * Chill is a software for social workers.
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

declare(strict_types=1);

/*
 * Chill is a software for social workers
 *
 * For the full copyright and license information, please view
 * the LICENSE file that was distributed with this source code.
 */

namespace Chill\CalendarBundle\Repository;

use Chill\CalendarBundle\Entity\Calendar;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;

class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface
{
    public function __construct(private readonly AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository, private readonly EntityManagerInterface $em) {}

    public function buildQueryByAccompanyingPeriod(AccompanyingPeriod $period, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate): QueryBuilder
    {
        $qb = $this->em->createQueryBuilder();
        $qb->from(Calendar::class, 'c');

        $andX = $qb->expr()->andX($qb->expr()->eq('c.accompanyingPeriod', ':period'));
        $qb->setParameter('period', $period);

        if (null !== $startDate) {
            $andX->add($qb->expr()->gte('c.startDate', ':startDate'));
            $qb->setParameter('startDate', $startDate);
        }

        if (null !== $endDate) {
            $andX->add($qb->expr()->lte('c.endDate', ':endDate'));
            $qb->setParameter('endDate', $endDate);
        }

        $qb->where($andX);

        return $qb;
    }

    public function buildQueryByAccompanyingPeriodIgnoredByDates(AccompanyingPeriod $period, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate): QueryBuilder
    {
        $qb = $this->em->createQueryBuilder();
        $qb->from(Calendar::class, 'c');

        $andX = $qb->expr()->andX($qb->expr()->eq('c.accompanyingPeriod', ':period'));
        $qb->setParameter('period', $period);

        if (null !== $startDate) {
            $andX->add($qb->expr()->lt('c.startDate', ':startDate'));
            $qb->setParameter('startDate', $startDate);
        }

        if (null !== $endDate) {
            $andX->add($qb->expr()->gt('c.endDate', ':endDate'));
            $qb->setParameter('endDate', $endDate);
        }

        $qb->where($andX);

        return $qb;
    }

    /**
     * Base implementation. The list of allowed accompanying period is retrieved "manually" from @see{AccompanyingPeriodACLAwareRepository}.
     */
    public function buildQueryByPerson(Person $person, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate): QueryBuilder
    {
        $qb = $this->em->createQueryBuilder()
            ->from(Calendar::class, 'c');

        $this->addQueryByPersonWithoutDate($qb, $person);

        // filter by date
        if (null !== $startDate) {
            $qb->andWhere($qb->expr()->gte('c.startDate', ':startDate'))
                ->setParameter('startDate', $startDate);
        }

        if (null !== $endDate) {
            $qb->andWhere($qb->expr()->lte('c.endDate', ':endDate'))
                ->setParameter('endDate', $endDate);
        }

        return $qb;
    }

    /**
     * Base implementation. The list of allowed accompanying period is retrieved "manually" from @see{AccompanyingPeriodACLAwareRepository}.
     */
    public function buildQueryByPersonIgnoredByDates(Person $person, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate): QueryBuilder
    {
        $qb = $this->em->createQueryBuilder()
            ->from(Calendar::class, 'c');

        $this->addQueryByPersonWithoutDate($qb, $person);

        // filter by date
        if (null !== $startDate) {
            $qb->andWhere($qb->expr()->lt('c.startDate', ':startDate'))
                ->setParameter('startDate', $startDate);
        }

        if (null !== $endDate) {
            $qb->andWhere($qb->expr()->gt('c.endDate', ':endDate'))
                ->setParameter('endDate', $endDate);
        }

        return $qb;
    }

    public function countByAccompanyingPeriod(AccompanyingPeriod $period, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate): int
    {
        $qb = $this->buildQueryByAccompanyingPeriod($period, $startDate, $endDate)->select('count(c)');

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

    public function countByPerson(Person $person, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate): int
    {
        return $this->buildQueryByPerson($person, $startDate, $endDate)
            ->select('COUNT(c)')
            ->getQuery()
            ->getSingleScalarResult();
    }

    public function countIgnoredByAccompanyingPeriod(AccompanyingPeriod $period, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate): int
    {
        $qb = $this->buildQueryByAccompanyingPeriodIgnoredByDates($period, $startDate, $endDate)->select('count(c)');

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

    public function countIgnoredByPerson(Person $person, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate): int
    {
        return $this->buildQueryByPersonIgnoredByDates($person, $startDate, $endDate)
            ->select('COUNT(c)')
            ->getQuery()
            ->getSingleScalarResult();
    }

    /**
     * @return array|Calendar[]
     */
    public function findByAccompanyingPeriod(AccompanyingPeriod $period, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], ?int $offset = null, ?int $limit = null): array
    {
        $qb = $this->buildQueryByAccompanyingPeriod($period, $startDate, $endDate)->select('c');

        foreach ($orderBy as $sort => $order) {
            $qb->addOrderBy('c.'.$sort, $order);
        }

        if (null !== $offset) {
            $qb->setFirstResult($offset);
        }

        if (null !== $limit) {
            $qb->setMaxResults($limit);
        }

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

    public function findByPerson(Person $person, ?\DateTimeImmutable $startDate, ?\DateTimeImmutable $endDate, ?array $orderBy = [], ?int $offset = null, ?int $limit = null): array
    {
        $qb = $this->buildQueryByPerson($person, $startDate, $endDate)
            ->select('c');

        foreach ($orderBy as $sort => $order) {
            $qb->addOrderBy('c.'.$sort, $order);
        }

        if (null !== $offset) {
            $qb->setFirstResult($offset);
        }

        if (null !== $limit) {
            $qb->setMaxResults($limit);
        }

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

    private function addQueryByPersonWithoutDate(QueryBuilder $qb, Person $person): void
    {
        // find the reachable accompanying periods for person
        $periods = $this->accompanyingPeriodACLAwareRepository->findByPerson($person, AccompanyingPeriodVoter::SEE);

        $qb
            ->where(
                $qb->expr()->orX(
                    // the calendar where the person is the main person:
                    $qb->expr()->eq('c.person', ':person'),
                    // when the calendar is in a reachable period, and contains person
                    $qb->expr()->andX(
                        $qb->expr()->in('c.accompanyingPeriod', ':periods'),
                        $qb->expr()->isMemberOf(':person', 'c.persons')
                    )
                )
            )
            ->setParameter('person', $person)
            ->setParameter('periods', $periods);
    }
}
