<?php

namespace CirrusSearch\Fallbacks;

use CirrusSearch\InterwikiResolver;
use CirrusSearch\Parser\AST\Visitor\QueryFixer;
use CirrusSearch\Parser\BasicQueryClassifier;
use CirrusSearch\Profile\ArrayPathSetter;
use CirrusSearch\Profile\SearchProfileException;
use CirrusSearch\Profile\SearchProfileService;
use CirrusSearch\Search\SearchMetricsProvider;
use CirrusSearch\Search\SearchQuery;
use Elastica\Client;
use Elastica\Query;
use Elastica\Search;

class IndexLookupFallbackMethod implements FallbackMethod, ElasticSearchRequestFallbackMethod, SearchMetricsProvider {
	use FallbackMethodTrait;

	/**
	 * @var string One of 'enabled' or 'metrics' indicating how
	 * the fallback operates. 'metrics' is for collecting data without
	 * using the result.
	 */
	private string $mode;

	/**
	 * @var SearchQuery
	 */
	private $query;

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

	/**
	 * @var array
	 */
	private $queryTemplate;

	/**
	 * @var string[]
	 */
	private $queryParams;

	/**
	 * @var string
	 */
	private $suggestionField;

	/**
	 * @var array
	 */
	private $profileParams;

	/**
	 * @var QueryFixer
	 */
	private $queryFixer;

	/**
	 * @var array
	 */
	private $searchMetrics = [];

	/**
	 * @var string[] Stored fields to request from elasticsearch
	 */
	private $storedFields;

	public function getMetrics(): array {
		return $this->searchMetrics;
	}

	/**
	 * @param Client $client
	 * @return Search|null null if no additional request is to be executed for this method.
	 * @see FallbackRunnerContext::getMethodResponse()
	 */
	public function getSearchRequest( Client $client ) {
		$fixablePart = $this->queryFixer->getFixablePart();
		if ( $fixablePart === null ) {
			return null;
		}
		$queryParams = array_map(
			function ( $v ) {
				switch ( $v ) {
					case 'query':
						return $this->queryFixer->getFixablePart();
					case 'wiki':
						return $this->query->getSearchConfig()->getWikiId();
					default:
						return $this->extractParam( $v );
				}
			},
			$this->queryParams
		);
		$arrayPathSetter = new ArrayPathSetter( $queryParams );
		$query = $arrayPathSetter->transform( $this->queryTemplate );
		$query = new Query( [ 'query' => $query ] );
		$query->setFrom( 0 )
			->setSize( 1 )
			->setSource( false )
			->setStoredFields( $this->storedFields );
		$search = new Search( $client );
		$search->setQuery( $query )
			->addIndex( $client->getIndex( $this->index ) );
		return $search;
	}

	/**
	 * @param string $keyAndValue
	 * @return mixed
	 */
	private function extractParam( $keyAndValue ) {
		$ar = explode( ':', $keyAndValue, 2 );
		if ( count( $ar ) != 2 ) {
			throw new SearchProfileException( "Invalid profile parameter [$keyAndValue]" );
		}
		[ $key, $value ] = $ar;
		switch ( $key ) {
			case 'params':
				$paramValue = $this->profileParams[$value] ?? null;
				if ( $paramValue == null ) {
					throw new SearchProfileException( "Missing profile parameter [$value]" );
				}
				return $paramValue;
			default:
				throw new SearchProfileException( "Unsupported profile parameter type [$key]" );
		}
	}

	/**
	 * @param string $mode Either 'enabled' or 'metrics'.
	 * @param SearchQuery $query
	 * @param string $index
	 * @param array $queryTemplate
	 * @param string $suggestionField
	 * @param string[] $queryParams
	 * @param string[] $metricFields Additional stored fields to request and
	 *  report with metrics.
	 * @param array $profileParams
	 */
	public function __construct(
		string $mode,
		SearchQuery $query,
		$index,
		$queryTemplate,
		$suggestionField,
		array $queryParams,
		array $metricFields,
		array $profileParams
	) {
		$this->mode = $mode;
		$this->query = $query;
		$this->index = $index;
		$this->queryTemplate = $queryTemplate;
		$this->suggestionField = $suggestionField;
		$this->queryParams = $queryParams;
		$this->profileParams = $profileParams;
		$this->queryFixer = QueryFixer::build( $this->query->getParsedQuery() );
		$this->storedFields = $metricFields;
		$this->storedFields[] = $suggestionField;
	}

	/**
	 * @param SearchQuery $query
	 * @param array $params
	 * @param InterwikiResolver|null $interwikiResolver
	 * @return FallbackMethod|null the method instance or null if unavailable
	 */
	public static function build( SearchQuery $query, array $params, ?InterwikiResolver $interwikiResolver = null ) {
		if ( !$query->isWithDYMSuggestion() ) {
			return null;
		}
		// TODO: Should this be tested at an upper level?
		if ( $query->getOffset() !== 0 ) {
			return null;
		}
		if ( !$query->getParsedQuery()->isQueryOfClass( BasicQueryClassifier::SIMPLE_BAG_OF_WORDS ) ) {
			// This method does not highlight the problem and thus is not able to use QueryFixer to fix parts of the query.
			// Ignore complex queries to avoid destructing them.
			return null;
		}
		if ( !isset( $params['profile'] ) ) {
			throw new SearchProfileException( "Missing mandatory field profile" );
		}

		$profileParams = $params['profile_params'] ?? [];

		$profile = $query->getSearchConfig()->getProfileService()
			->loadProfileByName( SearchProfileService::INDEX_LOOKUP_FALLBACK, $params['profile'] );
		'@phan-var array $profile';

		return new self(
			$params['mode'] ?? 'enabled',
			$query,
			$profile['index'],
			$profile['query'],
			$profile['suggestion_field'],
			$profile['params'],
			$profile['metric_fields'],
			$profileParams
		);
	}

	/**
	 * @param FallbackRunnerContext $context
	 * @return float
	 */
	public function successApproximation( FallbackRunnerContext $context ) {
		if ( $this->mode !== 'enabled' ) {
			return 0.0;
		}
		$rset = $this->extractMethodResponse( $context );
		if ( $rset === null || $rset->getResults() === [] ) {
			return 0.0;
		}
		$fields = $rset->getResults()[0]->getFields();
		$suggestion = $fields[$this->suggestionField][0] ?? null;
		if ( $suggestion === null ) {
			return 0.0;
		}
		// Metrics fields are everything except the suggestion field
		unset( $fields[$this->suggestionField] );
		if ( $fields ) {
			$this->searchMetrics += $fields;
		}
		return 0.5;
	}

	/**
	 * @param FallbackRunnerContext $context
	 * @return \Elastica\ResultSet|null null if there are no response or no results
	 */
	private function extractMethodResponse( FallbackRunnerContext $context ) {
		if ( !$context->hasMethodResponse() ) {
			return null;
		}

		return $context->getMethodResponse();
	}

	/**
	 * Rewrite the results,
	 * A costly call is allowed here, if nothing is to be done $previousSet
	 * must be returned.
	 *
	 * @param FallbackRunnerContext $context
	 * @return FallbackStatus
	 */
	public function rewrite( FallbackRunnerContext $context ): FallbackStatus {
		if ( $this->mode !== 'enabled' ) {
			// we are only collecting metrics
			return FallbackStatus::noSuggestion();
		}
		$previousSet = $context->getPreviousResultSet();
		if ( !$context->costlyCallAllowed() ) {
			// a method rewrote the query before us.
			return FallbackStatus::noSuggestion();
		}
		if ( $previousSet->getSuggestionQuery() !== null ) {
			// a method suggested something before us
			return FallbackStatus::noSuggestion();
		}
		$resultSet = $context->getMethodResponse();
		if ( !$resultSet->getResults() ) {
			return FallbackStatus::noSuggestion();
		}
		$res = $resultSet->getResults()[0];
		$suggestedQuery = $res->getFields()[$this->suggestionField][0] ?? null;
		if ( $suggestedQuery === null ) {
			return FallbackStatus::noSuggestion();
		}
		// Maybe rewrite
		return $this->maybeSearchAndRewrite( $context, $this->query, $suggestedQuery );
	}
}
