<?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;

use Chill\MainBundle\Entity\GroupCenter;
use Chill\MainBundle\Entity\User;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\ORM\QueryBuilder;

final readonly class UserRepository implements UserRepositoryInterface
{
    private EntityRepository $repository;

    private const FIELDS = [
        'id',
        'email',
        'enabled',
        'civility_id',
        'civility_abbreviation',
        'civility_name',
        'label',
        'mainCenter_id',
        'mainCenter_name',
        'mainScope_id',
        'mainScope_name',
        'userJob_id',
        'userJob_name',
        'currentLocation_id',
        'currentLocation_name',
        'mainLocation_id',
        'mainLocation_name',
    ];

    public function __construct(private EntityManagerInterface $entityManager, private Connection $connection)
    {
        $this->repository = $entityManager->getRepository(User::class);
    }

    public function countBy(array $criteria): int
    {
        return $this->repository->count($criteria);
    }

    public function countByActive(): int
    {
        return $this->countBy(['enabled' => true]);
    }

    public function countByNotHavingAttribute(string $key): int
    {
        $rsm = new ResultSetMapping();
        $rsm->addScalarResult('count', 'count');

        $sql = 'SELECT count(*) FROM users u WHERE NOT attributes ?? :key OR attributes IS NULL AND enabled IS TRUE';

        return $this->entityManager->createNativeQuery($sql, $rsm)->setParameter(':key', $key)->getSingleScalarResult();
    }

    public function countByUsernameOrEmail(string $pattern): int
    {
        $qb = $this->queryByUsernameOrEmail($pattern);

        $qb->select('COUNT(u)');

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

    public function find($id, $lockMode = null, $lockVersion = null): ?User
    {
        return $this->repository->find($id, $lockMode, $lockVersion);
    }

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

    /**
     * @throws Exception
     */
    public function findAllAsArray(string $lang): iterable
    {
        $sql = sprintf(<<<'SQL'
            SELECT
                u.id,
                u.username AS username,
                u.email AS email,
                u.enabled,
                u.civility_id,
                civility.abbreviation->>:lang AS civility_abbreviation,
                civility.name->>:lang AS civility_name,
                u.label,
                mainCenter.id AS mainCenter_id,
                mainCenter.name AS mainCenter_name,
                mainScope.id AS mainScope_id,
                mainScope.name->>:lang AS mainScope_name,
                userJob.id AS userJob_id,
                userJob.label->>:lang AS userJob_name,
                currentLocation.id AS currentLocation_id,
                currentLocation.name AS currentLocation_name,
                mainLocation.id AS mainLocation_id,
                mainLocation.name AS mainLocation_name,
                u.absenceStart
            FROM users u
            LEFT JOIN chill_main_civility civility ON u.civility_id = civility.id
            LEFT JOIN centers mainCenter ON u.maincenter_id = mainCenter.id
            LEFT JOIN chill_main_user_job_history userJobHistory ON u.id = userJobHistory.user_id AND tstzrange(userJobHistory.startdate, userJobHistory.enddate) @> NOW()
            LEFT JOIN chill_main_user_job userJob ON userJobHistory.job_id = userJob.id
            LEFT JOIN chill_main_user_scope_history userScopeHistory ON u.id = userScopeHistory.user_id AND tstzrange(userScopeHistory.startdate, userScopeHistory.enddate) @> NOW()
            LEFT JOIN scopes mainScope ON userScopeHistory.scope_id = mainScope.id
            LEFT JOIN chill_main_location currentLocation ON u.currentlocation_id = currentLocation.id
            LEFT JOIN chill_main_location mainLocation ON u.mainlocation_id = mainLocation.id
            ORDER BY u.label, u.id
        SQL);

        $query = $this->connection->prepare($sql);

        foreach ($query->executeQuery(['lang' => $lang])->iterateAssociative() as $u) {
            $converted = [];
            foreach (self::FIELDS as $f) {
                $converted[$f] = $u[strtolower($f)];
            }

            $converted['absenceStart'] = null !== $u['absencestart'] ? new \DateTimeImmutable($u['absencestart']) : null;

            /* @phpstan-ignore-next-line phpstan does not take into account that all required keys will be present */
            yield $converted;
        }
    }

    public function findAllUserACLAsArray(): iterable
    {
        $sql = <<<'SQL'
            SELECT
                u.id,
                u.username,
                u.email,
                u.label,
                u.enabled,
                c.id AS center_id,
                c.name AS center_name,
                pg.id AS permissionsGroup_id,
                pg.name AS permissionsGroup_name
            FROM users u
                LEFT JOIN user_groupcenter ON u.id = user_groupcenter.user_id
                LEFT JOIN group_centers ON user_groupcenter.groupcenter_id = group_centers.id
                LEFT JOIN centers c on group_centers.center_id = c.id
                LEFT JOIN permission_groups pg on group_centers.permissionsgroup_id = pg.id
            ORDER BY u.username, c.name, pg.name
        SQL;

        $query = $this->entityManager->getConnection()->executeQuery($sql);

        foreach ($query->iterateAssociative() as $u) {
            yield $u;
        }
    }

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

    /**
     * @return array|User[]
     */
    public function findByActive(?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
    {
        return $this->findBy(['enabled' => true], $orderBy, $limit, $offset);
    }

    /**
     * Find users which does not have a key on attribute column.
     *
     * @return array|User[]
     */
    public function findByNotHavingAttribute(string $key, ?int $limit = null, ?int $offset = null): array
    {
        $rsm = new ResultSetMappingBuilder($this->entityManager);
        $rsm->addRootEntityFromClassMetadata(User::class, 'u');

        $sql = 'SELECT '.$rsm->generateSelectClause().' FROM users u WHERE NOT attributes ?? :key OR attributes IS NULL AND enabled IS TRUE';

        if (null !== $limit) {
            $sql .= " LIMIT {$limit}";
        }

        if (null !== $offset) {
            $sql .= " OFFSET {$offset}";
        }

        return $this->entityManager->createNativeQuery($sql, $rsm)->setParameter(':key', $key)->getResult();
    }

    public function findByUsernameOrEmail(string $pattern, ?array $orderBy = [], ?int $limit = null, ?int $offset = null): array
    {
        $qb = $this->queryByUsernameOrEmail($pattern);

        $qb->select('u');

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

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

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

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

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

    public function findOneByUsernameOrEmail(string $pattern): ?User
    {
        $qb = $this->queryByUsernameOrEmail($pattern)->select('u');

        try {
            return $qb->getQuery()->getSingleResult();
        } catch (NoResultException) {
            return null;
        }
    }

    /**
     * Get the users having a specific flags.
     *
     * If provided, only the users amongst "filtered users" are searched. This
     * allows to make a first search amongst users based on role and center
     * and, then filter those users having some flags.
     *
     * @param User[] $amongstUsers
     */
    public function findUsersHavingFlags($flag, array $amongstUsers = []): array
    {
        $gcs = $this
            ->entityManager
            ->createQuery(
                'SELECT DISTINCT gc '.
                'FROM '.GroupCenter::class.' gc '.
                'JOIN gc.permissionsGroup pg '.
                'WHERE '.
                'JSONB_EXISTS_IN_ARRAY(pg.flags, :flag) = :true '
            )
            ->setParameters([
                'true' => true,
                'flag' => $flag,
            ])
            ->getResult();

        if (0 === \count($gcs)) {
            return [];
        }

        $qb = $this->entityManager->createQueryBuilder();
        $qb
            ->select('DISTINCT u')
            ->from(User::class, 'u')
            ->where("u.enabled = 'TRUE'");

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

        foreach ($gcs as $i => $gc) {
            $orx->add(':gc_'.$i.' MEMBER OF u.groupCenters');
            $qb->setParameter('gc_'.$i, $gc);
        }

        $qb->andWhere($orx);

        if ([] !== $amongstUsers) {
            $qb
                ->andWhere($qb->expr()->in('u', ':amongstUsers'))
                ->setParameter('amongstUsers', $amongstUsers);
        }

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

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

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

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

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

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

    private function queryByUsernameOrEmail(string $pattern): QueryBuilder
    {
        $qb = $this->entityManager->createQueryBuilder()->from(User::class, 'u');

        $searchByPattern = $qb->expr()->orX();

        $searchByPattern
            ->add($qb->expr()->like('u.usernameCanonical', 'CONCAT(\'%\', LOWER(UNACCENT(:pattern)),  \'%\')'))
            ->add($qb->expr()->like('u.emailCanonical', 'CONCAT(\'%\', LOWER(UNACCENT(:pattern)), \'%\')'));

        $qb
            ->where($searchByPattern)
            ->setParameter('pattern', $pattern);

        return $qb;
    }

    public function buildFilterBaseQuery(?string $queryString, array $isActive)
    {
        if (null !== $queryString) {
            $qb = $this->queryByUsernameOrEmail($queryString);
        } else {
            $qb = $this->entityManager->createQueryBuilder()->from(User::class, 'u');
        }

        // Add condition based on active/inactive status
        if (in_array('Active', $isActive, true) && !in_array('Inactive', $isActive, true)) {
            $qb->andWhere('u.enabled = true');
        } elseif (in_array('Inactive', $isActive, true) && !in_array('Active', $isActive, true)) {
            $qb->andWhere('u.enabled = false');
        }

        return $qb;
    }

    public function findFilteredUsers(
        ?string $queryString = null,
        array $isActive = ['active'],
        ?int $start = 0,
        ?int $limit = 50,
        ?array $orderBy = ['username' => 'ASC'],
    ): array {
        $qb = $this->buildFilterBaseQuery($queryString, $isActive);

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

    public function countFilteredUsers(
        ?string $queryString = null,
        array $isActive = ['active'],
    ): int {
        $qb = $this->buildFilterBaseQuery($queryString, $isActive);

        try {
            return $qb
                ->select('COUNT(u)')
                ->getQuery()->getSingleScalarResult();
        } catch (NoResultException|NonUniqueResultException $e) {
            throw new \LogicException('a count query should return one result', previous: $e);
        }
    }
}
