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

final readonly class PostalCodeRepository implements PostalCodeRepositoryInterface
{
    private EntityManagerInterface $entityManager;

    private EntityRepository $repository;

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

    public function countByPattern(string $pattern, ?Country $country): int
    {
        $query = $this->buildQueryByPattern($pattern, $country);
        $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): ?PostalCode
    {
        return $this->repository->find($id, $lockMode, $lockVersion);
    }

    public function findAll(): array
    {
        return $this->repository->findAll();
    }

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

    public function findByPattern(string $pattern, ?Country $country, ?int $start = 0, ?int $limit = 50): array
    {
        $query = $this->buildQueryByPattern($pattern, $country);

        $rsm = new ResultSetMappingBuilder($this->entityManager);
        $rsm->addRootEntityFromClassMetadata(PostalCode::class, 'cmpc');
        $query->addSelectClause($rsm->generateSelectClause());

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

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

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

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

    private function buildQueryByPattern(string $pattern, ?Country $country): SearchApiQuery
    {
        $pattern = trim($pattern);

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

        $query
            ->setFromClause('chill_main_postal_code cmpc')
            ->andWhereClause('cmpc.origin = 0');

        if (null !== $country) {
            $query->andWhereClause('cmpc.country_id = ?', [$country->getId()]);
        }

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

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

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

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

        return $query;
    }
}
