<?php

class XenES_Model_Elasticsearch extends XenForo_Model
{
	public static $optimizedGenericMapping = array(
		"_source" => array("enabled" => false),
		"properties" => array(
			"title" => array("type" => "string"),
			"message" => array("type" => "string"),
			"date" => array("type" => "long", "store" => "yes"),
			"user" => array("type" => "long", "store" => "yes"),
			"discussion_id" => array("type" => "long", "store" => "yes")
		)
	);

	public function getMappings()
	{
		$indexName = XenES_Api::getInstance()->getIndex();

		$results = XenES_Api::getMappings($indexName);
		if (isset($results->$indexName->mappings))
		{
			// elasticsearch 1.0 format
			return $results->$indexName->mappings;
		}
		else if (isset($results->$indexName))
		{
			return $results->$indexName;
		}
		else
		{
			return false;
		}
	}

	public function getAllSearchContentTypes()
	{
		$mappingTypes = array();
		$searchContentTypes = $this->getModelFromCache('XenForo_Model_Search')->getSearchContentTypes();
		foreach ($searchContentTypes AS $searchHandler)
		{
			if (!class_exists($searchHandler))
			{
				continue;
			}

			$searchHandler = XenForo_Search_DataHandler_Abstract::create($searchHandler);
			$mappingTypes = array_merge($mappingTypes, $searchHandler->getSearchContentTypes());
		}

		 return array_unique($mappingTypes);
	}

	public function getOptimizableMappings(array $mappingTypes = null, $mappings = null)
	{
		if ($mappingTypes === null)
		{
			$mappingTypes = $this->getAllSearchContentTypes();
		}
		if ($mappings === null)
		{
			$mappings = $this->getMappings();
		}

		$optimizable = array();

		foreach ($mappingTypes AS $type)
		{
			if (!$mappings || !isset($mappings->$type)) // no index or no mapping
			{
				$optimize = true;
			}
			else
			{
				$optimize = $this->_verifyMapping($mappings->$type, self::$optimizedGenericMapping);
			}

			if ($optimize)
			{
				$optimizable[] = $type;
			}
		}

		return $optimizable;
	}

	protected function _verifyMapping($actualMappingObj, array $expectedMapping)
	{
		foreach ($expectedMapping AS $name => $value)
		{
			if (!isset($actualMappingObj->$name))
			{
				return true;
			}

			if (is_array($value))
			{
				if ($this->_verifyMapping($actualMappingObj->$name, $value))
				{
					return true;
				}
			}
			else if ($value === 'yes')
			{
				if ($actualMappingObj->$name !== true && $actualMappingObj->$name !== 'yes')
				{
					return true;
				}
			}
			else if ($value === 'no')
			{
				if ($actualMappingObj->$name !== false && $actualMappingObj->$name !== 'no')
				{
					return true;
				}
			}
			else if ($actualMappingObj->$name !== $value)
			{
				return true;
			}
		}

		return false;
	}

	public function optimizeMapping($type, $deleteFirst = true, array $extra = array())
	{
		$mapping = XenForo_Application::mapMerge(self::$optimizedGenericMapping, $extra);
		$indexName = XenES_Api::getInstance()->getIndex();

		if ($deleteFirst)
		{
			XenES_Api::deleteIndex($indexName, $type);
			$this->_getDb()->query("
				DELETE FROM xf_es_search_failed
				WHERE content_type = ?
			", $type);
		}
		return XenES_Api::updateMapping($indexName, $type, $mapping, true);
	}

	public function getStats()
	{
		$indexName = XenES_Api::getInstance()->getIndex();
		return XenES_Api::stats($indexName);
	}

	public function getStemmingConfiguration()
	{
		$indexName = XenES_Api::getInstance()->getIndex();

		$results = XenES_Api::getSettings($indexName);
		if (isset($results->$indexName))
		{
			$settings = $results->$indexName->settings;

			if (isset($settings->{'index.analysis.analyzer.default.type'}))
			{
				$type = $settings->{'index.analysis.analyzer.default.type'};
			}
			else if (isset($settings->index->analysis->analyzer->default->type))
			{
				$type = $settings->index->analysis->analyzer->default->type;
			}
			else
			{
				$type = 'standard';
			}

			if (isset($settings->{'index.analysis.analyzer.default.language'}))
			{
				$language = $settings->{'index.analysis.analyzer.default.language'};
			}
			else if (isset($settings->index->analysis->analyzer->default->language))
			{
				$language = $settings->index->analysis->analyzer->default->language;
			}
			else
			{
				$language = 'English';
			}

			return array(
				'analyzer' => $type,
				'language' => $language
			);
		} else {
			return array(
				'analyzer' => 'standard',
				'language' => 'English'
			);
		}
	}

	public function recreateIndex()
	{
		$indexName = XenES_Api::getInstance()->getIndex();

		$indexSettings = XenES_Api::getSettings($indexName);
		if (isset($indexSettings->{$indexName}->settings))
		{
			$settings = array(
				// this converts to an array recursively
				'settings' => json_decode(json_encode($indexSettings->{$indexName}->settings), true)
			);
		}
		else
		{
			$settings = array();
		}

		XenES_Api::deleteIndex($indexName);
		XenES_Api::createIndex($indexName, $settings);

		foreach ($this->getOptimizableMappings() AS $type)
		{
			$this->optimizeMapping($type, false);
		}

		$this->_getDb()->query("TRUNCATE TABLE xf_es_search_failed");
	}

	public function getReindexableEntries($limit = 100)
	{
		return $this->fetchAllKeyed($this->limitQueryResults('
			SELECT *
			FROM xf_es_search_failed
			WHERE reindex_date <= ?
			ORDER BY reindex_date
		', $limit), 'search_failed_id', XenForo_Application::$time);
	}

	public function reattemptFailedIndex(
		$action, $contentType, $contentId,
		array $record = null, $previousFailCount,
		XenForo_Search_SourceHandler_Abstract $handler = null
	)
	{
		if (!$handler)
		{
			$handler = XenForo_Search_SourceHandler_Abstract::getDefaultSourceHandler();
		}

		if (!($handler instanceof XenES_Search_SourceHandler_ElasticSearch))
		{
			return false;
		}

		$indexName = XenES_Api::getInstance()->getIndex();

		if ($action == 'delete')
		{
			$result = XenES_Api::delete($indexName, $contentType, $contentId);
		}
		else if ($action == 'index' && $record)
		{
			$result = XenES_Api::index($indexName, $contentType, $contentId, $record);
		}
		else
		{
			return false;
		}

		$success = $handler->isIndexSuccessful($result);
		if (!$success)
		{
			$this->logFailedIndex($action, $contentType, $contentId, $record, $previousFailCount);
		}
		else
		{
			$this->_getDb()->query("
				DELETE FROM xf_es_search_failed
				WHERE content_type = ?
					AND content_id = ?
			", array($contentType, $contentId));
		}

		return $success;
	}

	public function logFailedIndex($action, $contentType, $contentId, array $record = null, $previousFailCount)
	{
		if ($previousFailCount >= 5)
		{
			XenForo_Error::logError("Indexing for $contentType $contentId failed with elasticsearch 5 times, skipped.");
			return false;
		}

		// delay 1, 2, 4, 8, 16 hours
		$reindexDate = XenForo_Application::$time + 3600 * pow(2, $previousFailCount);
		$failCount = $previousFailCount + 1;

		$this->_getDb()->query("
			INSERT INTO xf_es_search_failed
				(content_type, content_id, action, data, fail_count, reindex_date)
			VALUES
				(?, ?, ?, ?, ?, ?)
			ON DUPLICATE KEY UPDATE
				fail_count = VALUES(fail_count),
				reindex_date = VALUES(reindex_date)
		", array($contentType, $contentId, $action, serialize($record), $failCount, $reindexDate));

		return true;
	}

	public function runFailedReindexer($limit = 100)
	{
		$failed = $this->getReindexableEntries($limit);
		foreach ($failed AS $fail)
		{
			$record = @unserialize($fail['data']);

			$this->reattemptFailedIndex(
				$fail['action'], $fail['content_type'], $fail['content_id'], $record, $fail['fail_count']
			);
		}

		return $failed;
	}
}