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

use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserGroup;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;

class EntityWorkflowRepository implements ObjectRepository
{
    private readonly EntityRepository $repository;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->repository = $entityManager->getRepository(EntityWorkflow::class);
    }

    public function countByCc(User $user): int
    {
        $qb = $this->buildQueryByCc($user)->select('count(ew)');

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

    public function countByDest(User $user): int
    {
        $qb = $this->buildQueryByDest($user)->select('count(ew)');

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

    public function countByPreviousDestWithoutReaction(User $user): int
    {
        $qb = $this->buildQueryByPreviousDestWithoutReaction($user)->select('count(ew)');

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

    public function countByPreviousTransitionned(User $user): int
    {
        $qb = $this->buildQueryByPreviousTransitionned($user)->select('count(ew)');

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

    /**
     * @param bool|null $isFinal true to get only the entityWorkflow which is finalized, false to get the workflows that are not finalized, and null to ignore
     *
     * @throws \Doctrine\ORM\NoResultException
     * @throws \Doctrine\ORM\NonUniqueResultException
     */
    public function countBySubscriber(User $user, ?bool $isFinal = null): int
    {
        $qb = $this->buildQueryBySubscriber($user, $isFinal)->select('count(ew)');

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

    public function countRelatedWorkflows(array $relateds): int
    {
        $qb = $this->repository->createQueryBuilder('w');

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

        foreach ($relateds as $related) {
            $orX->add(
                $qb->expr()->andX(
                    $qb->expr()->eq('w.relatedEntityClass', ':entity_class_'.$i),
                    $qb->expr()->eq('w.relatedEntityId', ':entity_id_'.$i)
                )
            );
            $qb
                ->setParameter('entity_class_'.$i, $related['entityClass'])
                ->setParameter('entity_id_'.$i, $related['entityId']);
            ++$i;
        }
        $qb->where($orX);

        return $qb->select('COUNT(w)')->getQuery()->getSingleScalarResult();
    }

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

    /**
     * @return array|EntityWorkflow[]
     */
    public function findAll(): array
    {
        return $this->repository->findAll();
    }

    /**
     * @return list<EntityWorkflow>
     */
    public function findByRelatedEntity($entityClass, $relatedEntityId): array
    {
        $qb = $this->repository->createQueryBuilder('w');

        $query = $qb->where(
            $qb->expr()->andX(
                $qb->expr()->eq('w.relatedEntityClass', ':entity_class'),
                $qb->expr()->eq('w.relatedEntityId', ':entity_id'),
            )
        )->setParameter('entity_class', $entityClass)
            ->setParameter('entity_id', $relatedEntityId);

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

    /**
     * @param mixed|null $limit
     * @param mixed|null $offset
     *
     * @return array|EntityWorkflow[]
     */
    public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
    {
        return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
    }

    public function findByCc(User $user, ?array $orderBy = null, $limit = null, $offset = null): array
    {
        $qb = $this->buildQueryByCc($user)->select('ew');

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

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

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

    public function findByDest(User $user, ?array $orderBy = null, $limit = null, $offset = null): array
    {
        $qb = $this->buildQueryByDest($user)->select('ew');

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

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

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

    public function findByPreviousDestWithoutReaction(User $user, ?array $orderBy = null, $limit = null, $offset = null): array
    {
        $qb = $this->buildQueryByPreviousDestWithoutReaction($user)->select('ew');

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

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

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

    public function findByPreviousTransitionned(User $user, ?array $orderBy = null, $limit = null, $offset = null): array
    {
        $qb = $this->buildQueryByPreviousTransitionned($user)->select('ew')->distinct(true);

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

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

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

    /**
     * @param bool|null  $isFinal true to get only the entityWorkflow which is finalized, false to get the workflows that are not finalized, and null to ignore
     * @param mixed|null $limit
     * @param mixed|null $offset
     */
    public function findBySubscriber(User $user, ?bool $isFinal = null, ?array $orderBy = null, $limit = null, $offset = null): array
    {
        $qb = $this->buildQueryBySubscriber($user, $isFinal)->select('ew');

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

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

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

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

    /**
     * Finds workflows that are not finalized and are older than the specified date.
     *
     * @param \DateTimeImmutable $olderThanDate the date to compare against
     *
     * @return iterable<EntityWorkflow>
     */
    public function findWorkflowsWithoutFinalStepAndOlderThan(\DateTimeImmutable $olderThanDate): iterable
    {
        $qb = $this->repository->createQueryBuilder('sw');

        $qb->select('sw')
            // only the workflow which are not finalized
            ->where(sprintf('NOT EXISTS (SELECT 1 FROM %s ews WHERE ews.isFinal = TRUE AND ews.entityWorkflow = sw.id)', EntityWorkflowStep::class))
            ->andWhere(
                $qb->expr()->orX(
                    // only the workflow where all the transitions are older than transitionAt
                    sprintf(':olderThanDate > ALL (SELECT ews2.transitionAt FROM %s ews2 WHERE ews2.transitionAt IS NOT NULL AND ews2.entityWorkflow = sw)', EntityWorkflowStep::class),
                    // or the workflow which have only the initial step, with no transition
                    sprintf('1 = (SELECT COUNT(ews3.id) FROM %s ews3 WHERE ews3.currentStep = :initial AND ews3.transitionAt IS NULL AND ews3.entityWorkflow = sw)', EntityWorkflowStep::class),
                )
            )
            ->andWhere('sw.createdAt < :olderThanDate')
            ->setParameter('olderThanDate', $olderThanDate)
            ->setParameter('initial', 'initial')
        ;

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

    public function getClassName(): string
    {
        return EntityWorkflow::class;
    }

    private function buildQueryByCc(User $user): QueryBuilder
    {
        $qb = $this->repository->createQueryBuilder('ew');

        $qb->join('ew.steps', 'step');

        $qb->where(
            $qb->expr()->andX(
                $qb->expr()->isMemberOf(':user', 'step.ccUser'),
                $qb->expr()->isNull('step.transitionAfter'),
                $qb->expr()->eq('step.isFinal', "'FALSE'")
            )
        );

        $qb->setParameter('user', $user);

        return $qb;
    }

    private function buildQueryByDest(User $user): QueryBuilder
    {
        $qb = $this->repository->createQueryBuilder('ew');

        $qb->join('ew.steps', 'step');

        $qb->where(
            $qb->expr()->andX(
                $qb->expr()->orX(
                    $qb->expr()->isMemberOf(':user', 'step.destUser'),
                    $qb->expr()->isMemberOf(':user', 'step.destUserByAccessKey'),
                    $qb->expr()->exists(sprintf('SELECT 1 FROM %s ug WHERE ug MEMBER OF step.destUserGroups AND :user MEMBER OF ug.users', UserGroup::class))
                ),
                $qb->expr()->isNull('step.transitionAfter'),
                $qb->expr()->eq('step.isFinal', "'FALSE'")
            )
        );

        $qb->setParameter('user', $user);

        return $qb;
    }

    private function buildQueryByPreviousDestWithoutReaction(User $user): QueryBuilder
    {
        $qb = $this->repository->createQueryBuilder('ew');

        $qb->join('ew.steps', 'step');

        $qb->where(
            $qb->expr()->andX(
                $qb->expr()->isMemberOf(':user', 'step.destUser'),
                $qb->expr()->neq('step.transitionBy', ':user'),
            )
        );

        $qb->setParameter('user', $user);

        return $qb;
    }

    private function buildQueryByPreviousTransitionned(User $user): QueryBuilder
    {
        $qb = $this->repository->createQueryBuilder('ew');

        $qb->join('ew.steps', 'step');

        $qb->where(
            $qb->expr()->andX(
                $qb->expr()->eq('step.transitionBy', ':user'),
            )
        );

        $qb->setParameter('user', $user);

        return $qb;
    }

    private function buildQueryBySubscriber(User $user, ?bool $isFinal): QueryBuilder
    {
        $qb = $this->repository->createQueryBuilder('ew');

        $qb->where(
            $qb->expr()->orX(
                $qb->expr()->isMemberOf(':user', 'ew.subscriberToStep'),
                $qb->expr()->isMemberOf(':user', 'ew.subscriberToFinal'),
            )
        );

        $qb->setParameter('user', $user);

        if (null !== $isFinal) {
            if ($isFinal) {
                $qb->andWhere(sprintf('EXISTS (SELECT 1 FROM %s step WHERE step.isFinal = true AND ew = step.entityWorkflow)', EntityWorkflowStep::class));
            } else {
                $qb->andWhere(sprintf('NOT EXISTS (SELECT 1 FROM %s step WHERE step.isFinal = true AND ew = step.entityWorkflow)', EntityWorkflowStep::class));
            }
        }

        return $qb;
    }
}
