<?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\Entity\Center;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\TaskBundle\Security\Authorization\TaskVoter;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use function count;

/**
 * Class SingleTaskRepository.
 */
class SingleTaskRepository extends EntityRepository
{
    final public const DATE_STATUS_CURRENT = 'current';

    final public const DATE_STATUS_ENDED = 'ended';

    final public const DATE_STATUS_NOT_STARTED = 'not_started';

    final public const DATE_STATUS_WARNING = 'warning';

    final public const DATE_STATUSES = [
        self::DATE_STATUS_ENDED,
        self::DATE_STATUS_WARNING,
        self::DATE_STATUS_CURRENT,
        self::DATE_STATUS_NOT_STARTED,
    ];

    /**
     * @var AuthorizationHelper
     */
    protected $authorizationHelper;

    /**
     * Count the tasks for given parameters.
     *
     * The parameters are describe in @see SingleTaskRepository::filterByParameters.
     *
     * @see SingleTaskRepository::filterByParameters
     *
     * @param array $params
     *
     * @return int
     */
    public function countByParameters($params, ?User $currentUser = null)
    {
        $qb = $this->createQueryBuilder('st')
            ->select('COUNT(st)');

        $this->buildQuery($qb, $params, $currentUser);

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

    /**
     * Find task for given parameters.
     *
     * Available parameters:
     *
     * - `person` : filter by person associated with the task ;
     * - `user` : filter by user that created the task ;
     * - `scope` : filter by scope associated with the task ;
     * - `date_status`: type of task. To choose between :
     *      `ended`, `warning`, `current`, `not_started` ;
     * - `is_closed`: boolean. Indicate if the tasks must be closed (true) or
     *  opened
     * - `types`: string[] an array of task types
     *
     * @param type $params
     *
     * @return type
     */
    public function findByParameters($params, User $currentUser, int $firstResult = 0, int $maxResults = 50)
    {
        $qb = $this->createQueryBuilder('st');

        $this->buildQuery($qb, $params, $currentUser);

        $qb
            ->setMaxResults($maxResults)
            ->setFirstResult($firstResult);

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

    public function setAuthorizationHelper(AuthorizationHelper $authorizationHelper)
    {
        $this->authorizationHelper = $authorizationHelper;
    }

    protected function addTypeFilter(QueryBuilder $qb, $params)
    {
        $andWhere = $qb->expr()->andX();

        switch ($params['date_status']) {
            case self::DATE_STATUS_ENDED:
                $andWhere
                    ->add($this->buildNowIsAfterEndDate($qb));

                break;

            case self::DATE_STATUS_WARNING:
                $andWhere
                    ->add($this->buildNowIsAfterEndDate($qb, true))
                    ->add($this->buildNowIsAfterWarningDate($qb));

                break;

            case self::DATE_STATUS_CURRENT:
                // st.endDate is NULL or (st.endDate is not null and st.endDate < now))
                $andWhere
                    ->add($this->buildNowIsAfterEndDate($qb, true))
                    ->add($this->buildNowIsAfterWarningDate($qb, true))
                    ->add($this->buildNowIsAfterStartDate($qb, false));

                break;

            case self::DATE_STATUS_NOT_STARTED:
                $andWhere
                    ->add($this->buildNowIsAfterEndDate($qb, true))
                    ->add($this->buildNowIsAfterWarningDate($qb, true))
                    ->add($this->buildNowIsAfterStartDate($qb, true));
        }
        $qb->setParameter('now', new \DateTime('today'), Types::DATE_MUTABLE);
        $qb->andWhere($andWhere);
    }

    protected function buildACLQuery(QueryBuilder $qb, User $currentUser)
    {
        if (null === $this->authorizationHelper) {
            throw new \LogicException('Injecting the authorization helper is required to run this query. Please use dependency injection to initialize this repository or use the method `setAuthorizationHelper`');
        }

        $role = TaskVoter::SHOW;
        $qb->join('st.person', 'p');

        $centers = $this->authorizationHelper
            ->getReachableCenters($currentUser, $role);

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

        foreach ($centers as $center) {
            $circles = $this->authorizationHelper
                ->getReachableCircles($currentUser, $role, $center);

            $centerWhere = $qb->expr()->andX();

            $centerWhere->add($qb->expr()->eq('p.center', ':center_'.$i));
            $qb->setParameter('center_'.$i, $center);
            $centerWhere->add($qb->expr()->in('st.circle', ':circles_'.$i));
            $qb->setParameter('circles_'.$i, $circles);
            $where->add($centerWhere);
            ++$i;
        }

        $qb->where($where);
    }

    protected function buildQuery(QueryBuilder $qb, $params, ?User $currentUser = null)
    {
        if (null !== $currentUser) {
            $this->buildACLQuery($qb, $currentUser);
        }

        if (\array_key_exists('person', $params) && !empty($params['person'])) {
            $qb->andWhere($qb->expr()->eq('st.person', ':person'));
            $qb->setParameter('person', $params['person']);
        } elseif (\array_key_exists('center', $params)) {
            if ($params['center'] instanceof Center) {
                $qb->join('st.person', 'person');
                $qb->andWhere($qb->expr()->eq('person.center', ':center'));
                $qb->setParameter('center', $params['center']);
            } else {
                throw new \UnexpectedValueException("params 'center' should be an instance of ".Center::class);
            }
        }

        if (\array_key_exists('unassigned', $params) && true === $params['unassigned']) {
            if (\array_key_exists('user', $params) && !empty($params['user'])) {
                throw new \UnexpectedValueException('You should not require for unassigned tasks and tasks assigned to some user.');
            }

            $qb->andWhere($qb->expr()->isNull('st.assignee'));
        }

        if (\array_key_exists('user', $params) && !empty($params['user'])) {
            $qb->andWhere($qb->expr()->eq('st.assignee', ':user'));
            $qb->setParameter('user', $params['user']);
        }

        if (\array_key_exists('scope', $params) && !empty($params['scope'])) {
            $qb->andWhere($qb->expr()->eq('st.circle', ':scope'));
            $qb->setParameter('scope', $params['scope']);
        }

        if (\array_key_exists('types', $params) && null !== $params['types']) {
            if (\count($params['types']) > 0) {
                $qb->andWhere($qb->expr()->in('st.type', ':types'));
                $qb->setParameter('types', $params['types']);
            }
        }

        if (\array_key_exists('date_status', $params) && !empty($params['date_status'])) {
            $this->addTypeFilter($qb, $params);
        }

        if (\array_key_exists('is_closed', $params)) {
            $qb->andWhere($this->buildIsClosed($qb, !$params['is_closed']));
        }
    }

    private function buildIsClosed(QueryBuilder $qb, bool $negative = false)
    {
        if (false === $negative) {
            return $qb->expr()->eq('st.closed', "'TRUE'");
        }

        return $qb->expr()->eq('st.closed', "'FALSE'");
    }

    private function buildNowIsAfterEndDate(QueryBuilder $qb, $negative = false)
    {
        if (false === $negative) {
            return $qb->expr()->andX()
                ->add($qb->expr()->isNotNull('st.endDate'))
                ->add($qb->expr()->lte('st.endDate', ':now'));
        }

        return $qb->expr()->orX()
            ->add(
                $qb->expr()->andX()
                    ->add($qb->expr()->isNotNull('st.endDate'))
                    ->add($qb->expr()->gt('st.endDate', ':now'))
            )
            ->add($qb->expr()->isNull('st.endDate'));
    }

    private function buildNowIsAfterStartDate(QueryBuilder $qb, bool $negative = false)
    {
        if (false === $negative) {
            return $qb->expr()->orX()
                ->add($qb->expr()->lte('st.startDate', ':now'))
                ->add($qb->expr()->isNull('st.startDate'));
        }

        return
                $qb->expr()->andX()
                    ->add($qb->expr()->gt('st.startDate', ':now'))
                    ->add($qb->expr()->isNotNull('st.startDate'));
    }

    private function buildNowIsAfterWarningDate(QueryBuilder $qb, bool $negative = false)
    {
        if (false === $negative) {
            return $qb->expr()->andX()
                ->add(
                    $qb->expr()->lte(
                        $qb->expr()->diff('st.endDate', 'st.warningInterval'),
                        ':now'
                    )
                );
        }

        return $qb->expr()->orX()
            ->add(
                $qb->expr()->andX()
                    ->add($qb->expr()->isNotNull('st.endDate'))
                    ->add($qb->expr()->isNotNull('st.warningInterval'))
                    ->add(
                        $qb->expr()->gt(
                            $qb->expr()->diff('st.endDate', 'st.warningInterval'),
                            ':now'
                        )
                    )
            )
            ->add($qb->expr()->isNull('st.endDate'))
            ->add($qb->expr()->isNull('st.warningInterval'));
    }

    public function countByDistinctTypes()
    {
        $qb = $this->createQueryBuilder('st')
            ->select('COUNT(DISTINCT st.type)');

        $this->buildQuery($qb, []);

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

    public function findAllTaskDistinctTypes()
    {
        $qb = $this->createQueryBuilder('st')
            ->select('DISTINCT st.type');

        $this->buildQuery($qb, []);

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