<?php


namespace AutoAdmin\Bundle\Content\Repository;


use AutoAdmin\Bundle\Content\DependencyInjection\Compiler\ContentRepositoryCompilerPass;
use AutoAdmin\Bundle\Content\Entity\Site;
use AutoAdmin\Bundle\Content\Model\IdInterface;
use AutoAdmin\Bundle\Content\Model\PublishableInterface;
use AutoAdmin\Bundle\Content\Site\SiteContextHelper;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ManagerRegistry;
use Exception;
use InvalidArgumentException;
use ReflectionClass;
use ReflectionException;
use RuntimeException;


abstract class AbstractContentRepository extends ServiceEntityRepository
{
    protected object|string|null $entityClass = null;

    protected SiteContextHelper $contextHelper;

    /**
     * @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)
    {
        $this->contextHelper = $contextHelper;
    }

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

    public function applyOffset(QueryBuilder $qb, ?int $offset = 0)
    {
        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->applyCriteria($qb, $criteria);

        if (null === $orderBy) {
            $orderBy = ['_.id' => 'DESC'];
            if ((new ReflectionClass($this->getEntityName()))->implementsInterface(PublishableInterface::class)) {
                // Order by publishable in first
                $orderBy = ['_.publishable' => 'DESC'] + $orderBy;
            }
        }

        $this->applyOrder($qb, $orderBy);

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

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

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

        return $qb;
    }

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

        if (!isset($options['disable_site_filter']) || $options['disable_site_filter'] !== true) {
            $this->applySiteContext($qb);
        }

        return $qb;
    }

    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)
    {
        foreach ($criteria as $criterion => $value) {
            // split criterion with operator if there is one
            $operator = null;
            if (is_string($criterion) && strpos($criterion, ' ') !== false) {
                list($criterion, $operator) = explode(' ', $criterion, 2);
            }

            $paramName = str_replace('.', '', $criterion);
            if ($value === null) {
                $qb->andWhere($criterion . ' ' . ($operator ?? 'IS') . ' NULL');
            } 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 = [])
    {
        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);
            }
        }
    }

    /**
     * @throws ReflectionException
     */
    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;
    }

    /**
     * @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 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();
    }

    protected function getRunnableQuery(object $query)
    {
        if ($query instanceof QueryBuilder) {
            $query = $query->getQuery();
        }

        if (!$query instanceof Query) {
            throw new InvalidArgumentException('Only query or query builder types are expected.');
        }

        $params = [];
        foreach ($query->getParameters() as $index => $parameter) {
            $queryVal = $parameter->getValue();
            if ($queryVal instanceof Collection) {
                $collection = $arrayCollection = $queryVal->toArray();
                foreach ($collection as $idx => $item) {
                    if ($item instanceof IdInterface && $id = $item->getId()) {
                        $collection[$idx] = $id;
                    } elseif (is_object($item)) {
                        $collection[$idx] = gettype($item);
                    } elseif (is_string($item)) {
                        $collection[$idx] = $item;
                    } else {
                        $collection[$idx] = 'XXXXX';
                    }
                }
                $queryVal = implode(',', $collection);
                if (($first = array_values($arrayCollection)[0]) && is_object($first) && $class = get_class($first)) {
                    $queryVal = '/* ' . $class . '[] */' . $queryVal;
                }

            } elseif (is_object($queryVal)) {
                if ($queryVal instanceof IdInterface && $id = $queryVal->getId()) {
                    $queryVal = $id;
                }
            }
            $params[] = $queryVal;
        }

        array_unshift($params, str_replace('?', '%s', $query->getSql()));
        $qString = call_user_func_array('sprintf', $params);

        return $qString;
    }
}
