<?php

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\PersonBundle\Repository\AccompanyingPeriod;

use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;

class AccompanyingPeriodWorkRepository implements ObjectRepository
{
    private readonly EntityRepository $repository;

    public function __construct(private readonly EntityManagerInterface $em)
    {
        $this->repository = $em->getRepository(AccompanyingPeriodWork::class);
    }

    public function countByAccompanyingPeriod(AccompanyingPeriod $period): int
    {
        return $this->repository->countByAccompanyingPeriod($period);
    }

    public function countBySocialActionWithDescendants(SocialAction $action): int
    {
        $qb = $this->buildQueryBySocialActionWithDescendants($action);
        $qb->select('COUNT(g)');

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

    public function countNearEndDateByUser(User $user, \DateTimeImmutable $since, \DateTimeImmutable $until): int
    {
        return $this->buildQueryNearEndDateByUser($user, $since, $until)
            ->select('count(w)')->getQuery()->getSingleScalarResult();
    }

    public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
    {
        return $this->repository->createQueryBuilder($alias, $indexBy);
    }

    public function find($id): ?AccompanyingPeriodWork
    {
        return $this->repository->find($id);
    }

    public function findAll(): array
    {
        return $this->repository->findAll();
    }

    public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
    {
        return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
    }

    /**
     * @param mixed|null $orderBy
     * @param mixed|null $limit
     * @param mixed|null $offset
     *
     * @return AccompanyingPeriodWork[]
     */
    public function findByAccompanyingPeriod(AccompanyingPeriod $period, $orderBy = null, $limit = null, $offset = null): array
    {
        return $this->repository->findByAccompanyingPeriod($period, $orderBy, $limit, $offset);
    }

    /**
     * Return a list of accompanying period with a defined order:.
     *
     * * first, opened works
     * * then, closed works
     *
     * @param array{types?: list<SocialAction>, user?: list<User>, currentUser?: User, after?: \DateTimeImmutable|null, before?: \DateTimeImmutable|null} $filters
     *
     * @return AccompanyingPeriodWork[]
     */
    public function findByAccompanyingPeriodOpenFirst(AccompanyingPeriod $period, array $filters, int $limit = 10, int $offset = 0): array
    {
        $rsm = new ResultSetMappingBuilder($this->em);
        $rsm->addRootEntityFromClassMetadata(AccompanyingPeriodWork::class, 'w');

        $sql = "SELECT {$rsm} FROM chill_person_accompanying_period_work w
                    LEFT JOIN chill_person_accompanying_period_work_referrer AS rw ON accompanyingperiodwork_id = w.id
                    AND (rw.enddate IS NULL OR rw.enddate > CURRENT_DATE)
                    WHERE accompanyingPeriod_id = :periodId";

        // implement filters

        if ([] !== ($filters['types'] ?? [])) {
            $sql .= ' AND w.socialaction_id IN (:types)';
        }

        if ([] !== ($filters['user'] ?? [])) {
            $sql .= ' AND rw.user_id IN ('
                .implode(
                    ', ',
                    // we add a user_xx for each key of the 'user' list
                    array_map(fn (User $u, int $idx) => ':user_'.$idx, $filters['user'], array_keys($filters['user']))
                )
                .')';
        }

        if (isset($filters['currentUser'])) {
            $sql .= ' AND rw.user_id = :currentUser';
        }

        $sql .= " AND daterange(:after::date, :before::date) && daterange(w.startDate, w.endDate, '[]')";

        // if the start and end date were inversed, we inverse the order to avoid an error
        if (null !== ($filters['after'] ?? null) && null !== $filters['before'] && $filters['after'] > $filters['before']) {
            $before = $filters['after'];
            $after = $filters['before'];
        } else {
            $before = $filters['before'];
            $after = $filters['after'];
        }

        // set limit and offset
        $sql .= " ORDER BY
                     CASE WHEN w.enddate IS NULL THEN '-infinity'::timestamp ELSE 'infinity'::timestamp END ASC,
                     w.startdate DESC,
                     w.enddate DESC,
                     w.id DESC";

        $sql .= ' LIMIT :limit OFFSET :offset';

        $typeIds = [];
        foreach ($filters['types'] as $type) {
            $typeIds[] = $type->getId();
        }

        $nq = $this->em->createNativeQuery($sql, $rsm)
            ->setParameter('periodId', $period->getId(), Types::INTEGER)
            ->setParameter('types', $typeIds)
            ->setParameter('after', $after)
            ->setParameter('before', $before)
            ->setParameter('limit', $limit, Types::INTEGER)
            ->setParameter('offset', $offset, Types::INTEGER);

        if (isset($filters['currentUser'])) {
            $nq->setParameter('currentUser', $filters['currentUser']->getId());
        }


        foreach ($filters['user'] as $key => $user) {
            $nq->setParameter('user_'.$key, $user);
        }

        return $nq->getResult();
    }

    /**
     * Return a list of types of social actions associated to the accompanying period.
     *
     * @return array<SocialAction>
     */
    public function findActionTypeByPeriod(AccompanyingPeriod $period): array
    {
        $in = $this->em->createQueryBuilder();
        $in
            ->select('1')
            ->from(AccompanyingPeriodWork::class, 'apw');

        $in->andWhere('apw.accompanyingPeriod = :period')->setParameter('period', $period);

        // join between the embedded exist query and the main query
        $in->andWhere('apw.socialAction = sa');

        $qb = $this->em->createQueryBuilder()->setParameters($in->getParameters());
        $qb
            ->select('sa')
            ->from(SocialAction::class, 'sa')
            ->where(
                $qb->expr()->exists($in->getDQL())
            );

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

    public function findNearEndDateByUser(User $user, \DateTimeImmutable $since, \DateTimeImmutable $until, int $limit = 20, int $offset = 0): array
    {
        return $this->buildQueryNearEndDateByUser($user, $since, $until)
            ->select('w')
            ->setFirstResult($offset)
            ->setMaxResults($limit)
            ->getQuery()
            ->getResult();
    }

    public function findOneBy(array $criteria): ?AccompanyingPeriodWork
    {
        return $this->repository->findOneBy($criteria);
    }

    public function getClassName()
    {
        return AccompanyingPeriodWork::class;
    }

    private function buildQueryBySocialActionWithDescendants(SocialAction $action): QueryBuilder
    {
        $actions = $action->getDescendantsWithThis();

        $qb = $this->repository->createQueryBuilder('g');

        $orx = $qb->expr()->orX();
        $i = 0;

        foreach ($actions as $a) {
            $orx->add(":action_{$i} MEMBER OF g.socialActions");
            $qb->setParameter("action_{$i}", $a);
        }
        $qb->where($orx);

        return $qb;
    }

    private function buildQueryNearEndDateByUser(User $user, \DateTimeImmutable $since, \DateTimeImmutable $until): QueryBuilder
    {
        $qb = $this->repository->createQueryBuilder('w');

        $qb
            ->join('w.accompanyingPeriod', 'period')
            ->where(
                $qb->expr()->andX(
                    $qb->expr()->gte('w.endDate', ':since'),
                    $qb->expr()->lte('w.startDate', ':until'),
                    $qb->expr()->orX(
                        $qb->expr()->eq('period.user', ':user'),
                        $qb->expr()->exists(
                            'SELECT 1 FROM '.AccompanyingPeriodWork::class.' subw JOIN subw.referrersHistory subw_ref_history WHERE subw.id = w.id AND subw_ref_history.user = :user and subw.ref_history.endDate IS NULL'
                        )
                    )
                )
            )
            ->setParameters([
                'user' => $user,
                'since' => $since,
                'until' => $until,
            ]);

        return $qb;
    }
}
