<?php

declare(strict_types=1);

namespace MonsieurBiz\SyliusSearchPlugin\Model\Document\Index;

use Elastica\Exception\Connection\HttpException;
use Elastica\Exception\ResponseException;
use JoliCode\Elastically\Client;
use MonsieurBiz\SyliusSearchPlugin\Exception\ReadFileException;
use MonsieurBiz\SyliusSearchPlugin\Factory\ResultSet\ResultSetFactoryInterface;
use MonsieurBiz\SyliusSearchPlugin\Helper\AggregationHelperInterface;
use MonsieurBiz\SyliusSearchPlugin\Helper\FilterHelperInterface;
use MonsieurBiz\SyliusSearchPlugin\Helper\SortHelperInterface;
use MonsieurBiz\SyliusSearchPlugin\Model\ArrayObject;
use MonsieurBiz\SyliusSearchPlugin\Model\Config\GridConfigInterface;
use MonsieurBiz\SyliusSearchPlugin\Model\Document\ResultSetInterface;
use MonsieurBiz\SyliusSearchPlugin\Modifier\QueryModifierInterface;
use MonsieurBiz\SyliusSearchPlugin\Provider\SearchQueryProviderInterface;
use Psr\Log\LoggerInterface;
use Sylius\Component\Channel\Context\ChannelContextInterface;
use Symfony\Component\Yaml\Yaml;

class Search extends AbstractIndex implements SearchInterface
{
    public function __construct(
        Client $client,
        private readonly SearchQueryProviderInterface $searchQueryProvider,
        private readonly ChannelContextInterface $channelContext,
        private readonly LoggerInterface $logger,
        private readonly AggregationHelperInterface $aggregationHelper,
        private readonly SortHelperInterface $sortHelper,
        private readonly FilterHelperInterface $filterHelper,
        private readonly ResultSetFactoryInterface $resultSetFactory,
        private readonly QueryModifierInterface $queryModifier,
    ) {
        parent::__construct($client);
    }

    public function search(GridConfigInterface $gridConfig): ResultSetInterface
    {
        try {
            return $this->query($gridConfig, $this->getSearchQuery($gridConfig));
        } catch (ReadFileException $exception) {
            $this->logger->critical($exception->getMessage());

            return $this->resultSetFactory->create($gridConfig->getLimit(), $gridConfig->getPage());
        }
    }

    public function instant(GridConfigInterface $gridConfig): ResultSetInterface
    {
        try {
            return $this->query($gridConfig, $this->getInstantQuery($gridConfig));
        } catch (ReadFileException $exception) {
            $this->logger->critical($exception->getMessage());

            return $this->resultSetFactory->create($gridConfig->getLimit(), $gridConfig->getPage());
        }
    }

    public function taxon(GridConfigInterface $gridConfig): ResultSetInterface
    {
        try {
            return $this->query($gridConfig, $this->getTaxonQuery($gridConfig));
        } catch (ReadFileException $exception) {
            $this->logger->critical($exception->getMessage());

            return $this->resultSetFactory->create($gridConfig->getLimit(), $gridConfig->getPage());
        }
    }

    private function query(GridConfigInterface $gridConfig, array $query): ResultSetInterface
    {
        $query = $this->queryModifier->modify($query);

        try {
            $results = $this->getClient()->getIndex($this->getIndexName($gridConfig->getLocale()))->search(
                $query,
                $gridConfig->getLimit(),
            );
        } catch (HttpException $exception) {
            $this->logger->critical($exception->getMessage());

            return $this->resultSetFactory->create($gridConfig->getLimit(), $gridConfig->getPage());
        } catch (ResponseException $exception) {
            $this->logger->critical($exception->getMessage());

            return $this->resultSetFactory->create($gridConfig->getLimit(), $gridConfig->getPage());
        }

        return $this->resultSetFactory->create($gridConfig->getLimit(), $gridConfig->getPage(), $results, $gridConfig->getTaxon());
    }

    /**
     * Retrieve the query to send to Elasticsearch for search.
     *
     *
     * @throws ReadFileException
     */
    private function getSearchQuery(GridConfigInterface $gridConfig): array
    {
        $query = $this->searchQueryProvider->getSearchQuery();

        // Replace params
        $query = str_replace('{{QUERY}}', $gridConfig->getQuery(), $query);
        $query = str_replace('{{CHANNEL}}', $this->channelContext->getChannel()->getCode(), $query);

        // Convert query to array
        $query = $this->parseQuery($query);

        $appliedFilters = $this->filterHelper->buildFilters($gridConfig->getAppliedFilters());
        if ($gridConfig->haveToApplyManuallyFilters() && isset($appliedFilters['bool']['filter'])) {
            // Will retrieve filters after we applied the current ones
            $query['query']['bool']['filter'] = array_merge(
                $query['query']['bool']['filter'],
                $appliedFilters['bool']['filter'],
            );
        } elseif (count($appliedFilters) > 0) {
            // Will retrieve filters before we applied the current ones
            $query['post_filter'] = new ArrayObject($appliedFilters); // Use custom ArrayObject because Elastica make `toArray` on it.
        }

        // Manage limits
        $from = ($gridConfig->getPage() - 1) * $gridConfig->getLimit();
        $query['from'] = max(0, $from);
        $query['size'] = max(1, $gridConfig->getLimit());

        // Manage sorting
        $channelCode = $this->channelContext->getChannel()->getCode();
        foreach ($gridConfig->getSorting() as $field => $order) {
            $query['sort'][] = $this->sortHelper->getSortParamByField($field, $channelCode, $order);

            break; // only 1
        }

        // Manage filters
        $aggs = $this->aggregationHelper->buildAggregations($gridConfig->getFilters());
        if (count($aggs) === 0) {
            $query['aggs'] = $this->aggregationHelper->buildAggregations($gridConfig->getFilters());
        }

        return $query;
    }

    /**
     * Retrieve the query to send to Elasticsearch for instant search.
     *
     *
     * @throws ReadFileException
     */
    private function getInstantQuery(GridConfigInterface $gridConfig): array
    {
        $query = $this->searchQueryProvider->getInstantQuery();

        // Replace params
        $query = str_replace('{{QUERY}}', $gridConfig->getQuery(), $query);
        $query = str_replace('{{CHANNEL}}', $this->channelContext->getChannel()->getCode(), $query);

        // Convert query to array
        return $this->parseQuery($query);
    }

    /**
     * Retrieve the query to send to Elasticsearch for taxon search.
     *
     *
     * @throws ReadFileException
     */
    private function getTaxonQuery(GridConfigInterface $gridConfig): array
    {
        $query = $this->searchQueryProvider->getTaxonQuery();

        // Replace params
        $query = str_replace('{{TAXON}}', $gridConfig->getTaxon()->getCode(), $query);
        $query = str_replace('{{CHANNEL}}', $this->channelContext->getChannel()->getCode(), $query);

        // Convert query to array
        $query = $this->parseQuery($query);

        // Apply filters
        $appliedFilters = $this->filterHelper->buildFilters($gridConfig->getAppliedFilters());
        if ($gridConfig->haveToApplyManuallyFilters() && isset($appliedFilters['bool']['filter'])) {
            // Will retrieve filters after we applied the current ones
            $query['query']['bool']['filter'] = array_merge(
                $query['query']['bool']['filter'],
                $appliedFilters['bool']['filter'],
            );
        } elseif (count($appliedFilters) > 0) {
            // Will retrieve filters before we applied the current ones
            $query['post_filter'] = new ArrayObject($appliedFilters); // Use custom ArrayObject because Elastica make `toArray` on it.
        }

        // Manage limits
        $from = ($gridConfig->getPage() - 1) * $gridConfig->getLimit();
        $query['from'] = max(0, $from);
        $query['size'] = max(1, $gridConfig->getLimit());

        // Manage sorting
        $channelCode = $this->channelContext->getChannel()->getCode();
        foreach ($gridConfig->getSorting() as $field => $order) {
            $query['sort'][] = $this->sortHelper->getSortParamByField($field, $channelCode, $order, $gridConfig->getTaxon()->getCode());

            break; // only 1
        }

        // Manage filters
        $aggs = $this->aggregationHelper->buildAggregations($gridConfig->getFilters());
        if (count($aggs) > 0) {
            $query['aggs'] = $this->aggregationHelper->buildAggregations($gridConfig->getFilters());
        }

        return $query;
    }

    private function parseQuery(string $query): array
    {
        return Yaml::parse($query);
    }
}
