<?php declare(strict_types=1);

namespace Newland\Toubiz\Search\Neos\Backend\ElasticSearch;

use Flowpack\ElasticSearch\Domain\Model\AbstractType;
use Newland\Contracts\Neos\Search\SearchResult;
use Newland\Contracts\Neos\Search\SearchResultCollection;
use ONGR\ElasticsearchDSL\Query\Compound\BoolQuery;
use ONGR\ElasticsearchDSL\Query\FullText\SimpleQueryStringQuery;
use ONGR\ElasticsearchDSL\Query\TermLevel\TermQuery;
use ONGR\ElasticsearchDSL\Query\TermLevel\TermsQuery;
use ONGR\ElasticsearchDSL\Search;
use ONGR\ElasticsearchDSL\Sort\FieldSort;
use Neos\Flow\Annotations as Flow;

class Query
{

    /** @var BoolQuery */
    protected $query;

    /** @var Search */
    protected $search;

    /** @var AbstractType */
    protected $type;

    /**
     * @var array
     * @Flow\InjectConfiguration(path="elastic")
     */
    protected $configuration;

    public function injectType(ObjectTypeFactory $factory): void
    {
        $this->type = $factory->getType();
    }

    public function reset(): self
    {
        $this->search = (new Search())->addSort(new FieldSort('_score'));
        return $this;
    }

    public function searchFor(string $searchTerm): self
    {
        $this->search->addQuery(
            new SimpleQueryStringQuery(
                $this->prepareSearchTerm($searchTerm),
                $this->configuration['simpleQueryString']
            ),
            BoolQuery::MUST
        );
        return $this;
    }

    private function prepareSearchTerm(string $searchTerm): string
    {
        $searchTerm = trim($searchTerm);

        /*
         * Adds an asterisk to an end of a single word if there is none. Asterisks are used to denote
         * "anything after this" in elasticsearch. This enables users to search for the beginning of
         * a complex word without having to type it out completely.
         *
         * If the user has already entered a complex query or multiple words then the search term is not
         * modified.
         */
        $isSingleWordWithoutControlCharacters = preg_match('/\W/', $searchTerm) === false;
        if ($isSingleWordWithoutControlCharacters) {
            return $searchTerm . '*';
        }

        return $searchTerm;
    }

    public function inScopes(array $scopes): self
    {
        $this->search->addQuery(new TermsQuery('scopes', $scopes), BoolQuery::MUST);
        return $this;
    }

    public function limitedToTypes(array $types): self
    {
        $this->search->addQuery(new TermsQuery('type', $types), BoolQuery::MUST);
        return $this;
    }

    public function inLanguage(string $language): self
    {
        $this->search->addQuery(new TermQuery('language', $language), BoolQuery::MUST);
        return $this;
    }

    public function fromSource(string $source): self
    {
        $this->search->addQuery(new TermQuery('source.keyword', $source), BoolQuery::MUST);
        return $this;
    }

    public function notHavingIdentifier(array $identifiers): self
    {
        if (!empty($identifiers)) {
            $this->search->addQuery(new TermsQuery('identifier.keyword', $identifiers), BoolQuery::MUST_NOT);
        }
        return $this;
    }

    public function offsetLimit(int $offset, int $limit): self
    {
        $this->search->setFrom($offset);
        $this->search->setSize($limit);
        return $this;
    }

    public function paginate(int $page, int $pageSize): self
    {
        return $this->offsetLimit(
            max($page - 1, 0) * $pageSize,
            $pageSize
        );
    }

    public function run(): SearchResultCollection
    {
        $this->search->setExplain($this->configuration['explain'] ?? false);
        $response = $this->type->search($this->search->toArray());
        $hits = $response->getTreatedContent()['hits'] ?? [];

        $results = new SearchResultCollection($hits['total'] ?? 0);
        foreach ($hits['hits'] ?? [] as $hit) {
            $results->add(new SearchResult(
                $hit['_source']['identifier'],
                $hit['_source']['type'],
                $hit['_source']['title'],
                $hit['_source']['description'],
                $hit['_source']['content'],
                $hit['_source']['scopes'],
                $hit['_source']['language'],
                $hit['_source']['source'],
                $hit['_score'],
                $hit
            ));
        }

        return $results;
    }
}
