<?php

namespace App\Service;

use App\Tool;
use App\Tool\Utils;
use Symfony\Component\HttpFoundation\RequestStack;

class ElasticSearch
{
	/**
	 * @var ElasticSearch[]
	 */
	private static $instances;

	/**
	 * @var \Elasticsearch\Client
	 */
	protected $elasticClient = null;

	/**
	 * @var string
	 */
	protected $aliasPrefix = null;

	/**
	 * @var string
	 */
	protected $language = null;

	public static function getInstance(string $language): ElasticSearch
	{
		$key = $language;
		if (empty(self::$instances[$key])) {
			self::$instances[$key] = new self(null, $language);
		}

		return self::$instances[$key];
	}

	/**
	 * @throws \Exception
	 */
	public function __construct(RequestStack $requestStack = null, string $language = null)
	{
		// try to load language from $requestStack (injected), or $language (passed directly)
		if (null !== $requestStack) {
			$this->language = substr($requestStack->getCurrentRequest()->getLocale(), 0, 2);
		} elseif (null !== $language) {
			$this->language = $language;
		}
		// fallback to default language
		if (!$this->language) {
			$this->language = Utils::getDefaultLanguage();
		}

		$this->elasticClient = Tool\ElasticSearch::getClient();
		$this->aliasPrefix = Tool\ElasticSearch::getAliasPrefix();
	}

	public function fulltextSearch(array $filter = []): array
	{
		$baseOptions = ['index' => [
			$this->aliasPrefix . '_' . Tool\ElasticSearch::DOCUMENT_FULLTEXT_INDEX_KEY . '_' . $this->language,
			$this->aliasPrefix . '_' . Tool\ElasticSearch::OBJECT_FULLTEXT_INDEX_KEY.'_'.$this->language,
		]];
		if (!empty($filter['query'])) {
			$baseOptions['query']['multi_match']['query'] = $filter['query'];
			$baseOptions['query']['multi_match']['fields'] = ['title^4', 'keywords^3', 'description^2', 'content'];
		}
		$filteredQuery = $this->queryBase($baseOptions, $filter);

		$aggregations = [];
		$aggregations['type'] = $this->fulltextTypeAggregation($filteredQuery);

		if (!empty($filter['types'])) {
			$filteredQuery['body']['query']['bool']['filter']['bool']['must'][]['terms']['type'] = $filter['types'];
		}

		$pagination = $this->query($filteredQuery, $filter, true, true, true);
		$pagination['aggregations'] = $aggregations;

		return $pagination;
	}

	public function getNews(array $filters = []): array
	{
		$baseOptions = ['index' => $this->aliasPrefix . '_news_' . $this->language];

		if (!empty($filters['query'])) {
			$baseOptions['query']['multi_match']['query'] = $filters['query'];
			$baseOptions['query']['multi_match']['fields'] = ['name^1'];
		}

		$filteredQuery = $this->queryBase($baseOptions, $filters);

		return $this->query($filteredQuery, $filters);
	}

	private function fulltextTypeAggregation(array $query): array
	{
		$aggQuery = [];
		$aggQuery['type']['terms']['size'] = 100;
		$aggQuery['type']['terms']['field'] = 'type';

		$query['body']['aggs'] = $aggQuery;
		$result = $this->elasticClient->search($query);

		$types = [];
		foreach ($result['aggregations']['type']['buckets'] as $bucket) {
			$types[$bucket['key']] = $bucket['doc_count'];
		}

		return $types;
	}

	private function queryBase(array $options = [], array $filter = []): array
	{
		$filteredQuery['index'] = $options['index'];

		// set query for filtered query
		$query = (!empty($options['query'])) ? $options['query'] : null;
		if (is_array($query)) {
			$filteredQuery['body']['query']['bool']['must'] = $query;
		} elseif ('random_score' == $query) {
			$filteredQuery['body']['query']['bool']['must']['function_score']['functions'][0]['random_score'] = new \stdClass();
		} elseif (!empty($filter['ids_score']) && !empty($filter['ids'])) {
			$functionScore = [
				'boost_mode' => 'replace',
				'script_score' => [
					'script' => "count = ids.size(); return -ids.indexOf(Integer.parseInt(doc['id'].value));",
					'params' => [
						'ids' => $filter['ids'],
					],
				],
			];
			$filteredQuery['body']['query']['bool']['must']['function_score'] = $functionScore;
		} else {
			$filteredQuery['body']['query']['bool']['must']['match_all'] = new \stdClass();
		}

		// add filters
		$filteredQuery['body']['query']['bool']['filter']['bool']['must'][]['term']['published'] = true;

		return $filteredQuery;
	}

	private function constructSimplePagination(array $resultSet, int $limit, int $page, int $totalCount): array
	{
		$pagination = [];
		$pagination['current'] = $page;
		$pagination['pageCount'] = ($limit) ? (int) ceil($totalCount / $limit) : 1;
		if ($page > 1) {
			$pagination['previous'] = $page - 1;
		}
		if ($page < $pagination['pageCount']) {
			$pagination['next'] = $page + 1;
		}
		$pagination['totalCount'] = $totalCount;
		$pagination['first'] = 1;
		$pagination['last'] = $pagination['pageCount'];
		$pagination['items'] = empty($resultSet) ? [] : $resultSet['hits']['hits'];

		return $pagination;
	}

	/**
	 * @return array
	 */
	private function query(
		array $filteredQuery,
		array $filter,
		bool $foldedFallback = true,
		bool $paginate = true,
		bool $highlighting = false
	) {
		//add folded fallback fields
		$fulltextFields = [];
		if ($foldedFallback && !empty($filter['query'])) {
			if (!empty($filteredQuery['body']['query']['bool']['must']['multi_match']['fields'])) {
				$fulltextFields = $filteredQuery['body']['query']['bool']['must']['multi_match']['fields'];
			}
			$fields = [];
			foreach ($fulltextFields as $field) {
				$parts = explode('^', $field);
				$foldedField = $parts[0].'.folded';
				if (!empty($parts[1])) {
					$foldedField .= '^'.$parts[1];
				}
				$fields[] = $field;
				$fields[] = $foldedField;
			}
			if (!empty($fields)) {
				$filteredQuery['body']['query']['bool']['must']['multi_match']['fields'] = $fields;
			}
		}

		//get total count
		$countQuery = ['index' => $filteredQuery['index'], 'body' => ['query' => $filteredQuery['body']['query']]];
		$countResult = $this->elasticClient->count($countQuery);
		$totalCount = $countResult['count'];

		//sort, only when no searchQuery is set and some results matched and some sort is defined...
		if ($totalCount && empty($filter['query']) && !empty($filter['sort'])
		&& empty($filter['ids_score'])) {
			$sortStruct = $filter['sort'];
			if (is_array($sortStruct)) {
				foreach ($sortStruct as $key => $sort) {
					$filteredQuery['body']['sort'][$key] = $sort;
				}
			} elseif (stristr($sortStruct, ':')) {
				//$gps = explode(':', $sortStruct);
				$filteredQuery['body']['sort']['_geo_distance'] = [
					'location' => str_replace(':', ',', $sortStruct),
					'order' => 'asc',
					'unit' => 'km',
					'distance_type' => 'plane',
				];
			} else {
				//TODO define sorting types
			}
		}

		//add highlighting
		if ($highlighting && $fulltextFields && !empty($filter['query'])) {
			$highlight = ['fields' => []];
			foreach ($fulltextFields as $field) {
				$parts = explode('^', $field);
				$highlight['fields'][$parts[0]] = [
					'matched_fields' => [$parts[0]],
					'type' => 'fvh',
				];
				if ($foldedFallback) {
					$highlight['fields'][$parts[0]]['matched_fields'][] = $parts[0].'.folded';
				}
			}
			$filteredQuery['body']['highlight'] = $highlight;
		}

		//pagination
		$limit = !empty($filter['limit']) ? intval($filter['limit']) : 999;
		if ($paginate) {
			$page = !empty($filter['page']) ? intval($filter['page']) : 1;
			if ((int) $page > ceil($totalCount / $limit)) {
				$page = ceil($totalCount / $limit);
			}
			if ($page < 1) {
				$page = 1;
			}
			$filteredQuery['body']['from'] = isset($filter['offset']) ? ($page - 1) * $limit : 0;
			$filteredQuery['body']['size'] = isset($filter['offset']) ? $limit : $page * $limit;
		} else {
			$filteredQuery['body']['size'] = $limit;
		}
		//query
		$resultSet = $this->elasticClient->search($filteredQuery);
		if ($paginate) {
			return $this->constructSimplePagination($resultSet, $limit, $page, $totalCount);
		} else {
			return $resultSet;
		}
	}
}
