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

use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Pagination\PaginatorInterface;
use Chill\MainBundle\Serializer\Model\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Query\ResultSetMappingBuilder;

class SearchApi
{
    public function __construct(private readonly EntityManagerInterface $em, private readonly iterable $providers, private readonly PaginatorFactory $paginator) {}

    public function getResults(string $pattern, array $types, array $parameters): Collection
    {
        $queries = $this->findQueries($pattern, $types, $parameters);

        if (0 === \count($queries)) {
            throw new SearchApiNoQueryException($pattern, $types, $parameters);
        }

        $total = $this->countItems($queries);
        $paginator = $this->paginator->create($total);

        $rawResults = $this->fetchRawResult($queries, $types, $paginator);

        $this->prepareProviders($rawResults);
        $results = $this->buildResults($rawResults);

        return new Collection($results, $paginator);
    }

    private function buildCountQuery(array $queries): array
    {
        $query = 'SELECT SUM(c) AS count FROM ({union_unordered}) AS sq';
        $unions = [];
        $parameters = [];

        foreach ($queries as $q) {
            $unions[] = $q->buildQuery(true);
            $parameters = \array_merge($parameters, $q->buildParameters(true));
        }

        $unionUnordered = \implode(' UNION ', $unions);

        return [
            \strtr($query, ['{union_unordered}' => $unionUnordered]),
            $parameters,
        ];
    }

    private function buildResults(array $rawResults): array
    {
        $items = [];

        foreach ($rawResults as $r) {
            foreach ($this->providers as $p) {
                if ($p->supportsResult($r['key'], $r['metadata'])) {
                    $items[] = (new SearchApiResult($r['pertinence']))
                        ->setResult(
                            $p->getResult($r['key'], $r['metadata'], $r['pertinence'])
                        );

                    break;
                }
            }
        }

        return $items;
    }

    private function buildUnionQuery(array $queries, PaginatorInterface $paginator): array
    {
        $query = '{unions} ORDER BY pertinence DESC LIMIT ? OFFSET ?';
        $unions = [];
        $parameters = [];

        foreach ($queries as $q) {
            $unions[] = $q->buildQuery();
            $parameters = \array_merge($parameters, $q->buildParameters());
        }

        // add pagination limit
        $parameters[] = $paginator->getItemsPerPage();
        $parameters[] = $paginator->getCurrentPageFirstItemNumber();

        $union = \implode(' UNION ', $unions);

        return [
            \strtr($query, ['{unions}' => $union]),
            $parameters,
        ];
    }

    private function countItems($providers): int
    {
        [$countQuery, $parameters] = $this->buildCountQuery($providers);
        $rsmCount = new ResultSetMappingBuilder($this->em);
        $rsmCount->addScalarResult('count', 'count');
        $countNq = $this->em->createNativeQuery($countQuery, $rsmCount);
        $countNq->setParameters($parameters);

        return (int) $countNq->getSingleScalarResult();
    }

    private function fetchRawResult($queries, $types, PaginatorInterface $paginator): array
    {
        [$union, $parameters] = $this->buildUnionQuery($queries, $paginator);
        $rsm = new ResultSetMappingBuilder($this->em);
        $rsm->addScalarResult('key', 'key', Types::STRING)
            ->addScalarResult('metadata', 'metadata', Types::JSON)
            ->addScalarResult('pertinence', 'pertinence', Types::FLOAT);

        $nq = $this->em->createNativeQuery($union, $rsm);
        $nq->setParameters($parameters);

        return $nq->getResult();
    }

    private function findProviders(string $pattern, array $types, array $parameters): array
    {
        $providers = [];

        foreach ($this->providers as $provider) {
            if ($provider->supportsTypes($pattern, $types, $parameters)) {
                $providers[] = $provider;
            }
        }

        return $providers;
    }

    private function findQueries($pattern, array $types, array $parameters): array
    {
        return \array_map(
            static fn ($p) => $p->provideQuery($pattern, $parameters),
            $this->findProviders($pattern, $types, $parameters),
        );
    }

    private function prepareProviders(array $rawResults): void
    {
        $metadatas = [];
        $providers = [];

        foreach ($rawResults as $r) {
            foreach ($this->providers as $k => $p) {
                if ($p->supportsResult($r['key'], $r['metadata'])) {
                    $metadatas[$k][] = $r['metadata'];
                    $providers[$k] = $p;

                    break;
                }
            }
        }

        foreach ($metadatas as $k => $m) {
            $providers[$k]->prepare($m);
        }
    }
}
