<?php

namespace AutoAdmin\Bundle\Content\Repository;

use AutoAdmin\Bundle\Content\DependencyInjection\Compiler\ContentRepositoryCompilerPass;
use AutoAdmin\Bundle\Content\Entity\Site;
use AutoAdmin\Bundle\Content\Model\PublishableInterface;
use AutoAdmin\Bundle\Content\Model\PublishTimePeriodInterface;
use AutoAdmin\Bundle\Content\Model\SiteInterface;
use AutoAdmin\Bundle\Content\Site\SiteContextHelper;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\Query\Expr\Andx;
use Doctrine\ORM\Query\Expr\Orx;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use Exception;
use ReflectionClass;
use ReflectionException;
use RuntimeException;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Contracts\Service\Attribute\Required;

abstract class AbstractContentRepository extends ServiceEntityRepository
{
    protected object|string|null $entityClass = null;
    protected SiteContextHelper $contextHelper;
    protected Security $security;

    /**
     * @throws Exception
     */
    public function __construct(ManagerRegistry $registry, object|string|null $entityClass = null)
    {
        if ($entityClass === null) {
            throw new Exception('$entityClass is not provided in repository "'.get_class($this).'".');
        }

        $this->entityClass = $entityClass;

        parent::__construct($registry, $entityClass);
    }

    public function setSiteContextHelper(SiteContextHelper $contextHelper): void
    {
        $this->contextHelper = $contextHelper;
    }

    #[Required]
    public function setSecurity(Security $security): void
    {
        $this->security = $security;
    }

    public function applyLimit(QueryBuilder $qb, ?int $limit = 0): void
    {
        if (!empty($limit)) {
            $qb->setMaxResults($limit);
        }
    }

    public function applyOffset(QueryBuilder $qb, ?int $offset = 0): void
    {
        if (!empty($offset)) {
            $qb->setFirstResult($offset);
        }
    }

    /**
     * @throws ReflectionException
     */
    public function contentFindAll()
    {
        return $this->contentFindBy([]);
    }

    /**
     * @throws ReflectionException
     */
    public function contentFindBy(
        array $criteria,
        array $orderBy = null,
        ?int $limit = null,
        ?int $offset = null,
        array $groupBy = null
    ) {
        $qb = $this->createContentFindByQueryBuilder($criteria, $orderBy, $limit, $offset, $groupBy);

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

    /**
     * @throws ReflectionException
     */
    public function createContentFindByQueryBuilder(
        array $criteria,
        array $orderBy = null,
        ?int $limit = null,
        ?int $offset = null,
        array $groupBy = null
    ): QueryBuilder {
        $options = [
            'disable_site_filter' => !empty($criteria['_.site']),
        ];
        $qb = $this->createContentQueryBuilder('_', null, $options);

        $this->applyQueryBuilderConfig($qb, $criteria, $orderBy, $limit, $offset, $groupBy);

        return $qb;
    }

    protected function applyQueryBuilderConfig(
        QueryBuilder $qb,
        array $criteria,
        array $orderBy = null,
        ?int $limit = null,
        ?int $offset = null,
        array $groupBy = null
    ): void {
        $this->applyCriteria($qb, $criteria);

        $this->applyOrder($qb, $orderBy ?? []);

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

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

        if ($groupBy) {
            $this->applyGroup($qb, $groupBy);
        }
    }

    public function createContentQueryBuilder(
        string $alias = '_',
        ?string $indexBy = null,
        array $options = []
    ): QueryBuilder {
        $qb = $this->createQueryBuilder($alias, $indexBy);

        if (
            (new ReflectionClass($this->entityClass))->implementsInterface(SiteInterface::class)
            && (!isset($options['disable_site_filter']) || $options['disable_site_filter'] !== true)
        ) {
            $this->applySiteContext($qb);
        }

        return $qb;
    }

    public function createAutoContentQueryBuilder(
        string $alias = '_',
        ?string $indexBy = null,
        array $options = []
    ): QueryBuilder {
        $qb = $this->createContentQueryBuilder($alias, $indexBy, $options);

        $refl = new ReflectionClass($this->getEntityName());

        $criteria = [];

        if (!$this->security->isGranted('ROLE_ADMIN')) {
            // publishable
            if ($refl->implementsInterface(PublishableInterface::class)) {
                $criteria = array_merge_recursive($criteria, [
                    $alias . '.publishable' => true,
                ]);
            }

            // publish time period
            $expr = new Expr();
            if ($refl->implementsInterface(PublishTimePeriodInterface::class)) {
                $criteria = array_merge_recursive($criteria, [
                    $expr->andX(
                        $expr->orX(
                            $alias . '.publishStartDate IS NULL',
                            $expr->lte($alias . '.publishStartDate', ':_publish_current_date')
                        ),
                        $expr->orX(
                            $alias . '.publishEndDate IS NULL',
                            $expr->gte($alias . '.publishEndDate', ':_publish_current_date')
                        )
                    ),
                ]);
                $criteria['aa_parameters'] = array_merge_recursive($criteria['aa_parameters'] ?? [], [
                    '_publish_current_date' => new \DateTime(),
                ]);
            }
        }

        $this->applyCriteria($qb, $criteria);

        return $qb;
    }

    /**
     * @throws ReflectionException
     */
    public function createAutoContentFindByQueryBuilder(
        array $criteria,
        array $orderBy = null,
        ?int $limit = null,
        ?int $offset = null,
        array $groupBy = null,
        array $options = [],
    ): QueryBuilder {
        $qb = $this->createAutoContentQueryBuilder('_', null, $options);

        $this->applyQueryBuilderConfig($qb, $criteria, $orderBy, $limit, $offset, $groupBy);

        return $qb;
    }

    public function autoContentFindBy(
        array $criteria,
        array $orderBy = null,
        ?int $limit = null,
        ?int $offset = null,
        array $groupBy = null
    ) {
        $qb = $this->createAutoContentFindByQueryBuilder($criteria, $orderBy, $limit, $offset, $groupBy);

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

    public function autoContentFindAll()
    {
        return $this->autoContentFindBy([]);
    }

    public function createQueryBuilder($alias = '_', $indexBy = null): QueryBuilder
    {
        return parent::createQueryBuilder($alias, $indexBy);
    }

    public function applySiteContext(QueryBuilder $qb): ?QueryBuilder
    {
        $params = $qb->getParameters();
        /** @var Parameter $param */
        foreach ($params as $param) {
            if ($param->getName() == 'aa_site') {
                return null;
            }
        }

        $qb->andWhere($qb->getRootAliases()[0].'.site = :aa_site')
            ->setParameter('aa_site', $this->getSiteValue());

        return $qb;
    }

    protected function getSiteValue(): ?Site
    {
        if (empty($context = $this->contextHelper->getCurrentContext())) {
            throw new RuntimeException(
                'SiteContext was not provided to repository, '.
                'did you forgot to tag repository with "'.ContentRepositoryCompilerPass::CONTENT_REPOSITORY_TAG.'".'
            );
        }

        return $context->getSite();
    }

    public function applyCriteria(QueryBuilder $qb, array $criteria)
    {
        if (!empty($criteria['aa_parameters'])) {
            $parameters = $criteria['aa_parameters'];
            foreach ($parameters as $paramName => $paramValue) {
                $existingKeys = $qb->getParameters()->map(fn($v) => $v->getName())->toArray();
                if (in_array($paramName, $existingKeys)) {
                    throw new \Exception('Parameter '.$paramName.' already exists in applyCriteria().');
                }
                $qb->setParameter($paramName, $paramValue);
            }
        }
        unset($criteria['aa_parameters']);

        foreach ($criteria as $criterion => $value) {
            // handle Expr type conditions
            if (
                $value instanceof Andx ||
                $value instanceof Orx
            ) {
                $qb->andWhere($value);
                continue;
            }

            // split criterion with operator if there is one, permit to write ["_.field IN" => val]
            $operator = null;
            if (is_string($criterion) && strpos($criterion, ' ') !== false) {
                [$criterion, $operator] = explode(' ', $criterion, 2);
            }

            $paramName = str_replace('.', '', $criterion);
            $valueIsParam = is_string($value) && str_starts_with($value, ':');
            if ($value === null) {
                $qb->andWhere($criterion.' '.($operator ?? 'IS').' NULL');
            } elseif ($valueIsParam) {
                // here handle externally provided params (by passing parameter array via "aa_parameters" key of criteria)
                $qb->andWhere($criterion.' '.($operator ?? '=').' '.$value);
            } else {
                if (in_array($operator, ['IN', 'NOT IN'])) {
                    $qb->andWhere($criterion.' '.$operator.' (:'.$paramName.')');
                } else {
                    $qb->andWhere($criterion.' '.($operator ?? '=').' :'.$paramName);
                }

                $qb->setParameter($paramName, $value);
            }
        }
    }

    public function applyOrder(QueryBuilder $qb, array $orderBy = [], bool $addDefaults = true)
    {
        if (empty($orderBy) && $addDefaults) {
            $orderBy = ['_.id' => 'DESC'];
            if ((new ReflectionClass($this->getEntityName()))->implementsInterface(PublishableInterface::class)) {
                // Order by publishable in first
                $orderBy = ['_.publishable' => 'DESC'] + $orderBy;
            }
        }

        if (!empty($orderBy)) {
            foreach ($orderBy as $sorted => $direction) {
                $qb->addOrderBy($sorted, $direction);
            }
        }
    }

    public function applyGroup(QueryBuilder $qb, array $groupBy = [])
    {
        if (!empty($groupBy)) {
            foreach ($groupBy as $key) {
                $qb->addGroupBy($key);
            }
        }
    }

    public function contentFindOneBy(array $criteria, array $orderBy = null)
    {
        $qb = $this->createContentFindByQueryBuilder($criteria, $orderBy, 1);

        return (($res = $qb->getQuery()->execute()) && !empty($res[0])) ? $res[0] : null;
    }

    public function autoContentFindOneBy(array $criteria, array $orderBy = null)
    {
        $qb = $this->createAutoContentFindByQueryBuilder($criteria, $orderBy, 1);

        return (($res = $qb->getQuery()->execute()) && !empty($res[0])) ? $res[0] : null;
    }

    /**
     * @throws NonUniqueResultException
     * @throws NoResultException
     */
    public function contentCount(array $criteria = []): int
    {
        $qb = $this->createContentQueryBuilder();
        $qb->select('COUNT(_) as count');
        $this->applyCriteria($qb, $criteria);

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

    /**
     * @throws NonUniqueResultException
     * @throws NoResultException
     */
    public function autoContentCount(array $criteria = []): int
    {
        $qb = $this->createAutoContentQueryBuilder();
        $qb->select('COUNT(_) as count');
        $this->applyCriteria($qb, $criteria);

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

    /**
     * @throws ReflectionException
     */
    public function findIn(array $ids)
    {
        $qb = $this->createContentFindByQueryBuilder(['_.id IN' => $ids,]);

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

    /**
     * @throws ReflectionException
     */
    public function findNotIn(array $ids)
    {
        $criteria = [];
        if (!empty($ids)) {
            $criteria = ['_.id NOT IN' => $ids,];
        }

        $qb = $this->createContentFindByQueryBuilder($criteria);

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