<?php declare(strict_types=1);

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

use Flowpack\ElasticSearch\Domain\Factory\ClientFactory;
use Flowpack\ElasticSearch\Domain\Model\AbstractType;
use Flowpack\ElasticSearch\Domain\Model\GenericType;
use Flowpack\ElasticSearch\Domain\Model\Index;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Log\PsrLoggerFactory;
use Neos\Flow\Utility\Environment;
use Neos\Utility\Arrays;
use Newland\Toubiz\Search\Neos\Utility\ArrayUtility;
use Psr\Log\LoggerInterface;
use function Safe\preg_replace;

/**
 * @Flow\Scope("singleton")
 */
class ObjectTypeFactory
{

    /**
     * @var array|null
     * @Flow\InjectConfiguration(package="Neos.Flow", path="persistence.backendOptions")
     */
    protected $databaseSettings;

    /**
     * @Flow\Inject
     * @var ClientFactory
     */
    protected $clientFactory;

    /**
     * @var Environment
     * @Flow\Inject()
     */
    protected $environment;

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

    /** @var LoggerInterface */
    protected $logger;
    public function injectLogger(PsrLoggerFactory $loggerFactory): void
    {
        $this->logger = $loggerFactory->get('newlandSearch');
    }

    /** @var Index */
    private $index;

    public function initializeObject(): void
    {
        $indexName = $this->getIndexName();
        $this->index = $this->clientFactory->create()->findIndex($indexName);

        $configuration = (array) ($this->elasticSettings['indexSettings'] ?? []);
        $configuration['mappings'] = [
            $this->elasticSettings['typeName'] => $this->elasticSettings['typeMappings']
        ];

        $this->index->setSettingsKey('config');
        $this->index->injectSettings([ 'indexes' => [ 'default' => [ 'config' => $configuration ] ] ]);
    }

    public function getType(): AbstractType
    {
        return new GenericType($this->index, $this->elasticSettings['typeName']);
    }

    /**
     * Creates or updates the mappings with the correct settings associated.
     * This method will drop and recreate the index if the mappings have changed
     * and should only be called before indexing.
     */
    public function createOrUpdateIndex(): void
    {
        // Create index if it does not exist.
        if (!$this->index->exists()) {
            $this->logger->info('Index does not exist. Creating it.');
            $this->index->create();
            return;
        }

        // Apply mappings if they have changed.
        $mappingsHaveChanged = $this->mappingsHaveChanged();
        $settingsHaveChanged = $this->settingsHaveChanged();
        if ($mappingsHaveChanged || $settingsHaveChanged) {
            $this->logger->info(
                'Recreating index: ' . json_encode(compact('mappingsHaveChanged', 'settingsHaveChanged'))
            );
            $this->index->delete();
            $this->index->create();
        }
    }

    private function settingsHaveChanged(): bool
    {
        $response = $this->index->request('GET', '/_settings')->getTreatedContent();
        $settings = $response[$this->index->getName()];
        $changes = ArrayUtility::arrayRecursiveDiff($this->elasticSettings['indexSettings'], $settings);
        return count($changes) > 0;
    }

    private function mappingsHaveChanged(): bool
    {
        $response = $this->index->request('GET', '/_mappings')->getTreatedContent();
        $mappingsPath = sprintf('%s.mappings.%s', $this->index->getName(), $this->elasticSettings['typeName']);
        $mappings = Arrays::getValueByPath($response, $mappingsPath);
        $changes = ArrayUtility::arrayRecursiveDiff($this->elasticSettings['typeMappings'], $mappings);
        return count($changes) > 0;
    }

    /**
     * Returns the configured index name if exists or a index name based on the hostname & environment if not.
     */
    protected function getIndexName(): string
    {
        if ($this->elasticSettings['index'] ?? null) {
            return $this->elasticSettings['index'];
        }

        $context = 'unknown';
        if ($this->environment->getContext()->isDevelopment()) {
            $context = 'development';
        } elseif ($this->environment->getContext()->isTesting()) {
            $context = 'testing';
        } elseif ($this->environment->getContext()->isProduction()) {
            $context = 'production';
        }

        $name = implode('__', [
            gethostname(),
            $this->databaseSettings['user'],
            $this->databaseSettings['dbname'],
            $context,
        ]);

        return $this->normalizeName($name);
    }

    private function normalizeName(string $name): string
    {
        $name = strtolower($name);
        $name = preg_replace('/[^\w\-_]/', '-', $name);
        if (\is_array($name)) {
            return implode('-', $name);
        }
        return $name;
    }
}
