<?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\AddressReference;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Search\SearchApiQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\Persistence\ObjectRepository;

final readonly class AddressReferenceRepository implements ObjectRepository
{
    private EntityManagerInterface $entityManager;

    private EntityRepository $repository;

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

    public function countAll(): int
    {
        $qb = $this->repository->createQueryBuilder('ar');
        $qb->select('count(ar.id)');

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

    public function countByPostalCodePattern(PostalCode $postalCode, string $pattern): int
    {
        $query = $this->buildQueryByPostalCodePattern($postalCode, $pattern);
        $sql = $query->buildQuery(true);
        $rsm = new ResultSetMapping();
        $rsm->addScalarResult('c', 'c');

        $nq = $this->entityManager->createNativeQuery($sql, $rsm)->setParameters($query->buildParameters(true));

        return (int) $nq->getSingleResult()['c'];
    }

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

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

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

    /**
     * @return AddressReference[]|array
     */
    public function findByPostalCodePattern(PostalCode $postalCode, string $pattern, bool $simplify = false, int $start = 0, int $limit = 50): array
    {
        $query = $this->buildQueryByPostalCodePattern($postalCode, $pattern);

        if (!$simplify) {
            $rsm = new ResultSetMappingBuilder($this->entityManager);
            $rsm->addRootEntityFromClassMetadata(AddressReference::class, 'cma');
            $query->addSelectClause($rsm->generateSelectClause());
        } else {
            throw new \RuntimeException('not implemented');
        }

        $sql = \strtr(
            $query->buildQuery().'ORDER BY pertinence DESC, lpad(streetnumber, 10, \'0\') ASC OFFSET ? LIMIT ? ',
            // little hack for adding sql method to point
            ['cma.point AS point' => 'ST_AsGeojson(cma.point) AS point']
        );
        $parameters = [...$query->buildParameters(), $start, $limit];

        return $this->entityManager->createNativeQuery($sql, $rsm)
            ->setParameters($parameters)
            ->getResult();
    }

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

    public function getClassName()
    {
        return AddressReference::class;
    }

    private function buildQueryByPostalCodePattern(PostalCode $postalCode, string $pattern): SearchApiQuery
    {
        $pattern = \trim($pattern);

        if ('' === $pattern) {
            throw new \RuntimeException('the search pattern must not be empty');
        }
        $query = new SearchApiQuery();

        $query
            ->setFromClause('chill_main_address_reference cma')
            ->andWhereClause('postcode_id = ?', [$postalCode->getId()])
            ->andWhereClause('deletedAt IS NULL', []);

        $pertinenceClause = ['STRICT_WORD_SIMILARITY(addresscanonical, UNACCENT(?))'];
        $pertinenceArgs = [$pattern];
        $andWhere = [];
        $andWhereArgs = [];

        foreach (\explode(' ', $pattern) as $part) {
            $part = \trim($part);

            if ('' === $part) {
                continue;
            }

            $andWhere[] = "(addresscanonical LIKE '%' || UNACCENT(LOWER(?)) || '%')";
            $andWhereArgs[] = $part;
            $pertinenceClause[] =
                "(EXISTS (SELECT 1 FROM unnest(string_to_array(addresscanonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int";
            $pertinenceClause[] =
                '(addresscanonical LIKE UNACCENT(LOWER(?)))::int';
            array_push($pertinenceArgs, $part, $part);
        }
        $query
            ->setSelectPertinence(\implode(' + ', $pertinenceClause), $pertinenceArgs)
            ->andWhereClause(\implode(' AND ', $andWhere), $andWhereArgs);

        return $query;
    }
}
