<?php declare(strict_types=1);

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

use Neos\Flow\Annotations as Flow;
use Newland\Contracts\Neos\Search\IndexRecordModification;
use Newland\Contracts\Neos\Search\SearchBackend as SearchBackendContract;
use Newland\Contracts\Neos\Search\SearchRequest;
use Newland\Contracts\Neos\Search\SearchResult;
use Newland\Contracts\Neos\Search\SearchResultCollection;
use function Safe\preg_replace;

class SearchBackend implements SearchBackendContract
{
    /** @var string */
    protected $source;

    /**
     * @Flow\Inject()
     * @var Query
     */
    protected $query;

    /**
     * @Flow\Inject()
     * @var RecordBulkModifier
     */
    protected $modifier;

    /**
     * @var ObjectTypeFactory
     * @Flow\Inject()
     */
    protected $objectTypeFactory;

    public function initialize(): void
    {
        $this->objectTypeFactory->createOrUpdateIndex();
    }

    public function setSource(string $source): void
    {
        $this->source = $source;
    }

    /**
     * Sets index object content and creates/updates it in the index
     * via ElasticSearchConnector.
     *
     * @param IndexRecordModification $modification
     **/
    public function createOrUpdateIndexEntry(IndexRecordModification $modification): void
    {
        $identifier = $modification->getIdentifier() . '-' . $modification->getLanguage();

        // Set index object content.
        $indexContent = [
            'identifier' => $modification->getIdentifier(),
            'content' => $this->convertToIndexableString($modification->getContent()),
            'title' => $this->convertToIndexableString($modification->getTitle()),
            'description' => $this->convertToIndexableString($modification->getDescription()),
            'language' => $modification->getLanguage(),

            'type' => $modification->getType(),
            'scopes' => $modification->getScopes(),
            'source' => $this->source,
        ];

        // Pass index to ElasticSearchConnector.
        $this->modifier->updateOrCreate($indexContent, $identifier);
    }

    /**
     * Delete all index entries but the ones in $identifiersToRetain.
     *
     * @param string[] $identifiersToRetain
     **/
    public function deleteObsoleteIndexEntries(array $identifiersToRetain): void
    {
        for ($i = 0; $i < 250; $i++) {
            $results = $this->query->reset()
                ->fromSource($this->source)
                ->notHavingIdentifier($identifiersToRetain)
                ->offsetLimit(0, 10000)
                ->run();

            if ($results->isEmpty()) {
                break;
            }

            foreach ($results as $result) {
                /** @var SearchResult $result */
                $identifier = $result->getRawResult()['_id'] ?? null;
                if (\is_string($identifier)) {
                    $this->modifier->remove($identifier);
                }
            }
        }

        $this->modifier->flush();
    }

    protected function convertToIndexableString(string $input): string
    {
        // Remove HTML tags.
        $string = strip_tags($input);

        // Replace html entities like `&nbsp;`.
        $string = html_entity_decode($string);

        // Replace newlines with a space to shorten the string.
        $string = preg_replace('/[\r\n]+/', ' ', $string);

        if (\is_array($string)) {
            $string = implode(' ', $string);
        }

        return $string;
    }

    public function afterIndexing(): void
    {
        $this->modifier->flush();
    }

    public function search(SearchRequest $request): SearchResultCollection
    {
        return $this->query->reset()
            ->searchFor($request->getSearchTerm())
            ->limitedToTypes($request->getTypes())
            ->inLanguage($request->getLanguage())
            ->inScopes($request->getScopes())
            ->paginate($request->getPage(), $request->getItemsPerPage())
            ->run();
    }
}
