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

use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\TaskBundle\Entity\SingleTask;
use Chill\TaskBundle\Security\Authorization\TaskVoter;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Security;

final readonly class SingleTaskAclAwareRepository implements SingleTaskAclAwareRepositoryInterface
{
    public function __construct(private CenterResolverManagerInterface $centerResolverDispatcher, private EntityManagerInterface $em, private Security $security, private AuthorizationHelperInterface $authorizationHelper) {}

    public function buildBaseQuery(
        ?string $pattern = null,
        ?array $flags = [],
        ?array $types = [],
        ?array $users = [],
    ): QueryBuilder {
        $qb = $this->em->createQueryBuilder();
        $qb
            ->from(SingleTask::class, 't');

        if (null !== $pattern && '' !== $pattern) {
            $qb->andWhere($qb->expr()->like('LOWER(UNACCENT(t.title))', 'LOWER(UNACCENT(:pattern))'))
                ->setParameter('pattern', '%'.$pattern.'%');
        }

        if (null !== $users && \count($users) > 0) {
            $orXUser = $qb->expr()->orX();

            foreach ($users as $key => $user) {
                $orXUser->add(
                    $qb->expr()->eq('t.assignee', ':user_'.$key)
                );

                $qb->setParameter('user_'.$key, $user);
            }

            if ($orXUser->count() > 0) {
                $qb->andWhere($orXUser);
            }
        }

        if (null !== $types && \count($types) > 0) {
            $qb->andWhere($qb->expr()->in('t.type', ':types'));

            $qb->setParameter('types', $types);
        }

        if (null !== $flags && \count($flags) > 0) {
            $orXDate = $qb->expr()->orX();
            $orXState = $qb->expr()->orX();
            $now = new \DateTime();

            foreach ($flags as $key => $flag) {
                switch ($flag) {
                    case 'no-alert':
                        $orXDate
                            ->add(
                                $qb->expr()->orX(
                                    $qb->expr()->isNull('t.endDate'),
                                    $qb->expr()->gte('t.endDate - COALESCE(t.warningInterval, :intervalBlank)', ':now')
                                )
                            );
                        $qb
                            ->setParameter('intervalBlank', new \DateInterval('P0D'))
                            ->setParameter('now', $now);

                        break;

                    case 'warning':
                        $orXDate
                            ->add(
                                $qb->expr()->andX(
                                    $qb->expr()->not($qb->expr()->isNull('t.endDate')),
                                    $qb->expr()->not($qb->expr()->isNull('t.warningInterval')),
                                    $qb->expr()->lte('t.endDate - t.warningInterval', ':now'),
                                    $qb->expr()->gt('t.endDate', ':now')
                                )
                            );
                        $qb
                            ->setParameter('now', $now);

                        break;

                    case 'alert':
                        $orXDate
                            ->add(
                                $qb->expr()->andX(
                                    $qb->expr()->not($qb->expr()->isNull('t.endDate')),
                                    $qb->expr()->lte('t.endDate', ':now')
                                )
                            );
                        $qb
                            ->setParameter('now', $now);

                        break;

                    case 'state_new':
                        $orXState
                            ->add(
                                'JSONB_ARRAY_LENGTH(t.currentStates) = 0'
                            );

                        break;

                    case str_starts_with((string) $flag, 'state_'):
                        $state = \substr((string) $flag, 6);
                        $orXState
                            ->add(
                                "JSONB_EXISTS_IN_ARRAY(t.currentStates, :state_{$key}) = 'TRUE'"
                            );
                        $qb->setParameter("state_{$key}", $state);

                        break;

                    default:
                        throw new \LogicException("this flag is not supported: {$flag}");
                }
            }

            if ($orXDate->count() > 0) {
                $qb->andWhere($orXDate);
            }

            if ($orXState->count() > 0) {
                $qb->andWhere($orXState);
            }
        }

        return $qb;
    }

    public function buildQueryByCourse(
        AccompanyingPeriod $course,
        ?string $pattern = null,
        ?array $flags = [],
    ): QueryBuilder {
        $qb = $this->buildBaseQuery($pattern, $flags);

        return $qb
            ->andWhere($qb->expr()->eq('t.course', ':course'))
            ->setParameter('course', $course);
    }

    public function buildQueryByPerson(
        Person $person,
        ?string $pattern = null,
        ?array $flags = [],
    ): QueryBuilder {
        $qb = $this->buildBaseQuery($pattern, $flags);

        return $qb
            ->andWhere($qb->expr()->eq('t.person', ':person'))
            ->setParameter('person', $person);
    }

    public function buildQueryMyTasks(
        ?string $pattern = null,
        ?array $flags = [],
    ): QueryBuilder {
        $qb = $this->buildBaseQuery($pattern, $flags);

        return $qb
            ->andWhere($qb->expr()->eq('t.assignee', ':user'))
            ->setParameter('user', $this->security->getUser());
    }

    public function countByAllViewable(
        ?string $pattern = null,
        ?array $flags = [],
        ?array $types = [],
        ?array $users = [],
    ): int {
        $qb = $this->buildBaseQuery($pattern, $flags, $types, $users);

        return $this
            ->addACLGlobal($qb)
            ->select('COUNT(t)')
            ->getQuery()->getSingleScalarResult();
    }

    public function countByCourse(
        AccompanyingPeriod $course,
        ?string $pattern = null,
        ?array $flags = [],
    ): int {
        $qb = $this->buildQueryByCourse($course, $pattern, $flags);

        return $this
            ->addACL($qb, $course)
            ->select('COUNT(t)')
            ->getQuery()->getSingleScalarResult();
    }

    public function countByCurrentUsersTasks(
        ?string $pattern = null,
        ?array $flags = [],
    ): int {
        return $this->buildQueryMyTasks($pattern, $flags)
            ->select('COUNT(t)')
            ->getQuery()->getSingleScalarResult();
    }

    public function countByPerson(
        Person $person,
        ?string $pattern = null,
        ?array $flags = [],
    ): int {
        $qb = $this->buildQueryByPerson($person, $pattern, $flags);

        return $this
            ->addACL($qb, $person)
            ->select('COUNT(t)')
            ->getQuery()->getSingleScalarResult();
    }

    public function findByAllViewable(
        ?string $pattern = null,
        ?array $flags = [],
        ?array $types = [],
        ?array $users = [],
        ?int $start = 0,
        ?int $limit = 50,
        ?array $orderBy = [],
    ): array {
        $qb = $this->buildBaseQuery($pattern, $flags, $types, $users);
        $qb = $this->addACLGlobal($qb);

        return $this->getResult($qb, $start, $limit, $orderBy);
    }

    public function findByCourse(
        AccompanyingPeriod $course,
        ?string $pattern = null,
        ?array $flags = [],
        ?int $start = 0,
        ?int $limit = 50,
        ?array $orderBy = [],
    ): array {
        $qb = $this->buildQueryByCourse($course, $pattern, $flags);
        $qb = $this->addACL($qb, $course);

        return $this->getResult($qb, $start, $limit, $orderBy);
    }

    public function findByCurrentUsersTasks(
        ?string $pattern = null,
        ?array $flags = [],
        ?int $start = 0,
        ?int $limit = 50,
        ?array $orderBy = [],
    ): array {
        $qb = $this->buildQueryMyTasks($pattern, $flags);

        return $this->getResult($qb, $start, $limit, $orderBy);
    }

    public function findByPerson(
        Person $person,
        ?string $pattern = null,
        ?array $flags = [],
        ?int $start = 0,
        ?int $limit = 50,
        ?array $orderBy = [],
    ): array {
        $qb = $this->buildQueryByPerson($person, $pattern, $flags);
        $qb = $this->addACL($qb, $person);

        return $this->getResult($qb, $start, $limit, $orderBy);
    }

    public function getResult(
        QueryBuilder $qb,
        ?int $start = 0,
        ?int $limit = 50,
        ?array $orderBy = [],
    ): array {
        $qb->select('t');

        $qb
            ->setFirstResult($start)
            ->setMaxResults($limit);

        foreach ($orderBy as $field => $direction) {
            $qb->addOrderBy('t.'.$field, $direction);
        }

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

    private function addACL(
        QueryBuilder $qb,
        $entity,
    ): QueryBuilder {
        foreach ($this->centerResolverDispatcher->resolveCenters($entity) as $center) {
            $scopes = $this->authorizationHelper->getReachableScopes(
                $this->security->getUser(),
                TaskVoter::SHOW,
                $center
            );

            $qb->andWhere($qb->expr()->in('t.circle', ':scopes'))
                ->setParameter('scopes', $scopes);
        }

        return $qb;
    }

    private function addACLGlobal(
        QueryBuilder $qb,
    ): QueryBuilder {
        $allowedCenters = $this->authorizationHelper
            ->getReachableCenters($this->security->getUser(), TaskVoter::SHOW);

        if ([] === $allowedCenters) {
            $qb
                ->andWhere($qb->expr()->lt('t.id', ':falseid'))
                ->setParameter('falseid', -1);
        }

        $qb->leftJoin('t.person', 'person')
            ->leftJoin('t.course', 'course')
            ->leftJoin('course.participations', 'participation')
            ->leftJoin('participation.person', 'person_p')
            ->leftJoin('person.centerCurrent', 'center_current_person')
            ->leftJoin('person_p.centerCurrent', 'center_current_participation');
        $qb->distinct(true);

        $k = 0;
        $orX = $qb->expr()->orX();

        foreach ($allowedCenters as $center) {
            $allowedScopes = $this->authorizationHelper->getReachableScopes(
                $this->security->getUser(),
                TaskVoter::SHOW,
                $center
            );

            $and = $qb->expr()->andX(
                $qb->expr()->orX(
                    $qb->expr()->eq('center_current_person.center', ':center_'.$k),
                    $qb->expr()->eq('center_current_participation.center', ':center_'.$k)
                ),
                $qb->expr()->in('t.circle', ':scopes_'.$k)
            );
            $qb
                ->setParameter('center_'.$k, $center)
                ->setParameter('scopes_'.$k, $allowedScopes);
            $orX->add($and);

            ++$k;
        }
        $qb->andWhere($orX);

        return $qb;
    }
}
