<?php

namespace CirrusSearch\Search;

use CirrusSearch\CirrusDebugOptions;
use CirrusSearch\CirrusSearchHookRunner;
use CirrusSearch\ExternalIndex;
use CirrusSearch\Fallbacks\FallbackRunner;
use CirrusSearch\OtherIndexesUpdater;
use CirrusSearch\Parser\BasicQueryClassifier;
use CirrusSearch\Profile\SearchProfileService;
use CirrusSearch\Query\Builder\FilterBuilder;
use CirrusSearch\Search\Fetch\FetchPhaseConfigBuilder;
use CirrusSearch\Search\Rescore\BoostFunctionBuilder;
use CirrusSearch\Search\Rescore\RescoreBuilder;
use CirrusSearch\SearchConfig;
use CirrusSearch\WarningCollector;
use Elastica\Aggregation\AbstractAggregation;
use Elastica\Query\AbstractQuery;
use MediaWiki\MainConfigNames;
use MediaWiki\MediaWikiServices;
use Wikimedia\Assert\Assert;

/**
 * The search context, maintains the state of the current search query.
 *
 * @license GPL-2.0-or-later
 */

/**
 * The SearchContext stores the various states maintained
 * during the query building process.
 */
class SearchContext implements WarningCollector, FilterBuilder {
	/**
	 * @var SearchConfig
	 */
	private $config;

	/**
	 * @var int[]|null list of namespaces
	 */
	private $namespaces;

	/**
	 * @var string
	 */
	private $profileContext = SearchProfileService::CONTEXT_DEFAULT;

	/**
	 * @var string[]
	 */
	private $profileContextParams = [];

	/**
	 * @var string rescore profile to use
	 */
	private $rescoreProfile;

	/**
	 * @var BoostFunctionBuilder[] Extra scoring builders to use.
	 */
	private $extraScoreBuilders = [];

	/**
	 * @var bool Could this query possibly return results?
	 */
	private $resultsPossible = true;

	/**
	 * @var int[] List of features in the user suplied query string. Features are
	 *  held in the array key, value is how "complex" the feature is.
	 */
	private $syntaxUsed = [];

	/**
	 * @var AbstractQuery[] List of filters that query results must match
	 */
	private $filters = [];

	/**
	 * @var AbstractQuery[] List of filters that query results must not match
	 */
	private $notFilters = [];

	/**
	 * @var AbstractQuery|null Query that should be used for highlighting if different
	 *  from the query used for selecting.
	 */
	private $highlightQuery;

	/**
	 * @var AbstractQuery[] queries that don't use Elastic's "query string" query,
	 *  for more advanced highlighting (e.g. match_phrase_prefix for regular
	 *  quoted strings).
	 */
	private $nonTextHighlightQueries = [];

	/**
	 * @var AbstractQuery|null phrase rescore query
	 */
	private $phraseRescoreQuery;

	/**
	 * @var AbstractQuery|null main query. null defaults to MatchAll
	 */
	private $mainQuery;

	/**
	 * @var AbstractQuery[] Queries that don't use Elastic's "query string" query, for
	 *  more advanced searching (e.g. match_phrase_prefix for regular quoted strings).
	 */
	private $nonTextQueries = [];

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

	/**
	 * @var bool Should this search limit results to the local wiki?
	 */
	private $limitSearchToLocalWiki = false;

	/**
	 * @var int The number of seconds to cache results for
	 */
	private $cacheTtl = 0;

	/**
	 * @var string The original search
	 */
	private $originalSearchTerm;

	/**
	 * @var string The users search term with keywords removed
	 */
	private $cleanedSearchTerm;

	/**
	 * @var Escaper
	 */
	private $escaper;

	/**
	 * @var FetchPhaseConfigBuilder
	 */
	private $fetchPhaseBuilder;

	/**
	 * @var int[] weights of different syntaxes
	 */
	private static $syntaxWeights = [
		// Deep category searches can have thousands of terms
		'deepcategory' => 20,
		'full_text' => 10,
		'more_like' => 100,
		'near_match' => 10,
		'prefix' => 2,
		// regex is really tough
		'regex' => PHP_INT_MAX,
		// depends, filtered semantic is pretty expensive
		'semantic' => 20,
	];

	/**
	 * @var array[] Warnings to be passed into StatusValue::warning()
	 */
	private $warnings = [];

	/**
	 * @var string name of the fulltext query builder profile
	 */
	private $fulltextQueryBuilderProfile;

	/**
	 * @var bool Have custom options that effect the search results been set
	 *  outside the defaults from config?
	 */
	private $isDirty = false;

	/**
	 * @var ResultsType|FullTextResultsType Type of the result for the context.
	 */
	private $resultsType;

	/**
	 * @var AbstractAggregation[] Aggregations to perform
	 */
	private $aggs = [];

	/**
	 * @var CirrusDebugOptions
	 */
	private $debugOptions;

	/**
	 * @var FallbackRunner|null
	 */
	private $fallbackRunner;
	/**
	 * @var CirrusSearchHookRunner|null
	 */
	private $cirrusSearchHookRunner;

	/**
	 * @var bool
	 */
	private $trackTotalHits = true;

	/**
	 * @param SearchConfig $config
	 * @param int[]|null $namespaces
	 * @param CirrusDebugOptions|null $options
	 * @param FallbackRunner|null $fallbackRunner
	 * @param FetchPhaseConfigBuilder|null $fetchPhaseConfigBuilder
	 * @param CirrusSearchHookRunner|null $cirrusSearchHookRunner
	 */
	public function __construct(
		SearchConfig $config,
		?array $namespaces = null,
		?CirrusDebugOptions $options = null,
		?FallbackRunner $fallbackRunner = null,
		?FetchPhaseConfigBuilder $fetchPhaseConfigBuilder = null,
		?CirrusSearchHookRunner $cirrusSearchHookRunner = null
	) {
		$this->config = $config;
		$this->namespaces = $namespaces;
		$this->debugOptions = $options ?? CirrusDebugOptions::defaultOptions();
		$this->fallbackRunner = $fallbackRunner ?? FallbackRunner::noopRunner();
		$this->fetchPhaseBuilder = $fetchPhaseConfigBuilder ?? new FetchPhaseConfigBuilder( $config );
		$this->loadConfig();
		$this->cirrusSearchHookRunner = $cirrusSearchHookRunner ?? new CirrusSearchHookRunner(
			MediaWikiServices::getInstance()->getHookContainer() );
	}

	/**
	 * Return a copy of this context with a new configuration.
	 *
	 * @param SearchConfig $config The new configuration
	 * @return self
	 */
	public function withConfig( SearchConfig $config ) {
		$other = clone $this;
		$other->config = $config;
		$other->fetchPhaseBuilder = $this->fetchPhaseBuilder->withConfig( $config );
		if ( $other->resultsType instanceof FullTextResultsType ) {
			$other->resultsType = $this->resultsType->withFetchPhaseBuilder( $other->fetchPhaseBuilder );
		}
		$other->loadConfig();

		return $other;
	}

	private function loadConfig(): void {
		$this->escaper = new Escaper(
			$this->config->get( MainConfigNames::LanguageCode ),
			$this->config->get( 'CirrusSearchAllowLeadingWildcard' )
		);
	}

	public function __clone() {
		if ( $this->mainQuery ) {
			$this->mainQuery = clone $this->mainQuery;
		}
	}

	/**
	 * Have custom options that effect the search results been set outside the
	 * defaults from config?
	 *
	 * @return bool
	 */
	public function isDirty() {
		return $this->isDirty;
	}

	/**
	 * @return SearchConfig the Cirrus config object
	 */
	public function getConfig() {
		return $this->config;
	}

	/**
	 * mediawiki namespace id's being requested.
	 * NOTE: this value may change during the Searcher process.
	 *
	 * @return int[]|null
	 */
	public function getNamespaces() {
		return $this->namespaces;
	}

	/**
	 * set the mediawiki namespace id's
	 *
	 * @param int[]|null $namespaces array of integer
	 */
	public function setNamespaces( $namespaces ) {
		$this->isDirty = true;
		$this->namespaces = $namespaces;
	}

	/**
	 * @return string
	 */
	public function getProfileContext() {
		return $this->profileContext;
	}

	/**
	 * @return string[]
	 */
	public function getProfileContextParams(): array {
		return $this->profileContextParams;
	}

	/**
	 * @param string $profileContext
	 * @param string[] $contextParams
	 */
	public function setProfileContext( $profileContext, array $contextParams = [] ) {
		$this->isDirty = $this->isDirty ||
			$this->profileContext !== $profileContext ||
			$this->profileContextParams !== $contextParams;
		$this->profileContext = $profileContext;
		$this->profileContextParams = $contextParams;
	}

	/**
	 * @return string the rescore profile to use
	 */
	public function getRescoreProfile() {
		if ( $this->rescoreProfile === null ) {
			$this->rescoreProfile = $this->config->getProfileService()
				->getProfileName( SearchProfileService::RESCORE, $this->profileContext, $this->profileContextParams );
		}
		return $this->rescoreProfile;
	}

	/**
	 * @param string $rescoreProfile the rescore profile to use
	 */
	public function setRescoreProfile( $rescoreProfile ) {
		$this->isDirty = true;
		$this->rescoreProfile = $rescoreProfile;
	}

	/**
	 * @return bool Could this query possibly return results?
	 */
	public function areResultsPossible() {
		return $this->resultsPossible;
	}

	/**
	 * @param bool $possible Could this query possible return results? Defaults to true
	 *  if not called.
	 */
	public function setResultsPossible( $possible ) {
		$this->isDirty = true;
		$this->resultsPossible = $possible;
	}

	/**
	 * @param string|null $type type of syntax to check, null for any type
	 * @return bool True when the query uses $type kind of syntax
	 */
	public function isSyntaxUsed( $type = null ) {
		if ( $type === null ) {
			return $this->syntaxUsed !== [];
		}
		return isset( $this->syntaxUsed[$type] );
	}

	/**
	 * @return bool true if a special keyword or syntax was used in the query
	 */
	public function isSpecialKeywordUsed() {
		// full_text is not considered a special keyword
		// TODO: investigate using BasicQueryClassifier::SIMPLE_BAG_OF_WORDS instead
		return array_diff_key( $this->syntaxUsed, [
			'full_text' => true,
			'full_text_simple_match' => true,
			'full_text_querystring' => true,
			BasicQueryClassifier::SIMPLE_BAG_OF_WORDS => true,
			BasicQueryClassifier::SIMPLE_PHRASE => true,
			BasicQueryClassifier::BAG_OF_WORDS_WITH_PHRASE => true,
		] ) !== [];
	}

	/**
	 * @return string[] List of syntax used in the query
	 */
	public function getSyntaxUsed() {
		return array_keys( $this->syntaxUsed );
	}

	/**
	 * @return string Text description of syntax used by query.
	 */
	public function getSyntaxDescription() {
		return implode( ',', $this->getSyntaxUsed() );
	}

	/**
	 * @param string $feature Name of a syntax feature used in the query string
	 * @param int|null $weight How "complex" is this feature.
	 */
	public function addSyntaxUsed( $feature, $weight = null ) {
		$this->isDirty = true;
		$this->syntaxUsed[$feature] = $weight ?? self::$syntaxWeights[$feature] ?? 1;
	}

	/**
	 * @return string The type of search being performed, ex: full_text, near_match, prefix, etc.
	 * Using getSyntaxUsed() is better in most cases.
	 */
	public function getSearchType() {
		if ( !$this->syntaxUsed ) {
			return 'full_text';
		}
		arsort( $this->syntaxUsed );
		// Return the first heaviest syntax
		return key( $this->syntaxUsed );
	}

	/**
	 * @param AbstractQuery $filter Query results must match this filter
	 */
	public function addFilter( AbstractQuery $filter ) {
		$this->isDirty = true;
		$this->filters[] = $filter;
	}

	/**
	 * @param AbstractQuery $filter Query results must not match this filter
	 */
	public function addNotFilter( AbstractQuery $filter ) {
		$this->isDirty = true;
		$this->notFilters[] = $filter;
	}

	/**
	 * @param AbstractQuery|null $query Query that should be used for highlighting if different
	 *  from the query used for selecting.
	 */
	public function setHighlightQuery( ?AbstractQuery $query = null ) {
		$this->isDirty = true;
		$this->highlightQuery = $query;
	}

	/**
	 * @param AbstractQuery $query queries that don't use Elastic's "query
	 * string" query, for more advanced highlighting (e.g. match_phrase_prefix
	 * for regular quoted strings).
	 */
	public function addNonTextHighlightQuery( AbstractQuery $query ) {
		$this->isDirty = true;
		$this->nonTextHighlightQueries[] = $query;
	}

	public function getFetchPhaseBuilder(): FetchPhaseConfigBuilder {
		return $this->fetchPhaseBuilder;
	}

	/**
	 * @param ResultsType $resultsType
	 * @param AbstractQuery $mainQuery Will be combined with highlighting query
	 *  to provide highlightable terms.
	 * @return array|null Fetch portion of query to be sent to elasticsearch
	 */
	public function getHighlight( ResultsType $resultsType, AbstractQuery $mainQuery ) {
		$highlight = $resultsType->getHighlightingConfiguration( [] );
		if ( !$highlight ) {
			return null;
		}

		$query = $this->getHighlightQuery( $mainQuery );
		if ( $query ) {
			$highlight['highlight_query'] = $query->toArray();
		}

		return $highlight;
	}

	/**
	 * @param AbstractQuery $mainQuery
	 * @return AbstractQuery|null Query that should be used for highlighting if different
	 *  from the query used for selecting.
	 */
	private function getHighlightQuery( AbstractQuery $mainQuery ) {
		if ( !$this->nonTextHighlightQueries ) {
			// If no explicit highlight query is provided elastic
			// will fallback to $mainQuery without specifying it.
			return $this->highlightQuery;
		}

		$bool = new \Elastica\Query\BoolQuery();
		// If no explicit highlight query is provided we
		// need to include the main query along with
		// the non-text queries to highlight those fields.
		$bool->addShould( $this->highlightQuery ?: $mainQuery );
		foreach ( $this->nonTextHighlightQueries as $nonTextHighlightQuery ) {
			$bool->addShould( $nonTextHighlightQuery );
		}

		return $bool;
	}

	/**
	 * rescore_query has to be in array form before we send it to Elasticsearch but it is way
	 * easier to work with if we leave it in query form until now
	 *
	 * @return array[] Rescore configurations as used by elasticsearch.
	 */
	public function getRescore() {
		$rescores = ( new RescoreBuilder( $this, $this->cirrusSearchHookRunner ) )->build();
		$result = [];
		foreach ( $rescores as $rescore ) {
			$rescore['query']['rescore_query'] = $rescore['query']['rescore_query']->toArray();
			$result[] = $rescore;
		}

		return $result;
	}

	/**
	 * @return AbstractQuery The primary query to be sent to elasticsearch. Includes
	 *  the main quedry, non text queries, and any additional filters.
	 */
	public function getQuery() {
		if ( !$this->nonTextQueries ) {
			$mainQuery = $this->mainQuery ?: new \Elastica\Query\MatchAll();
		} else {
			$mainQuery = new \Elastica\Query\BoolQuery();
			if ( $this->mainQuery ) {
				$mainQuery->addMust( $this->mainQuery );
			}
			foreach ( $this->nonTextQueries as $nonTextQuery ) {
				$mainQuery->addMust( $nonTextQuery );
			}
		}
		$filters = $this->filters;
		if ( $this->getNamespaces() ) {
			// We must take an array_values here, or it can be json-encoded into an object instead
			// of a list which elasticsearch will interpret as terms lookup.
			$filters[] = new \Elastica\Query\Terms( 'namespace', array_values( $this->getNamespaces() ) );
		}

		// Wrap $mainQuery in a filtered query if there are any filters
		$unifiedFilter = Filters::unify( $filters, $this->notFilters );
		if ( $unifiedFilter !== null ) {
			if ( !( $mainQuery instanceof \Elastica\Query\BoolQuery ) ) {
				$bool = new \Elastica\Query\BoolQuery();
				$bool->addMust( $mainQuery );
				$mainQuery = $bool;
			}
			$mainQuery->addFilter( $unifiedFilter );
		}

		return $mainQuery;
	}

	/**
	 * @param AbstractQuery $query The primary query to be passed to
	 *  elasticsearch.
	 */
	public function setMainQuery( AbstractQuery $query ) {
		$this->isDirty = true;
		$this->mainQuery = $query;
	}

	/**
	 * @param \Elastica\Query\AbstractQuery $match Queries that don't use Elastic's
	 * "query string" query, for more advanced searching (e.g.
	 *  match_phrase_prefix for regular quoted strings).
	 */
	public function addNonTextQuery( \Elastica\Query\AbstractQuery $match ) {
		$this->isDirty = true;
		$this->nonTextQueries[] = $match;
	}

	/**
	 * @return SearchQuery
	 */
	public function getSearchQuery() {
		return $this->searchQuery;
	}

	/**
	 * @return bool Should this search limit results to the local wiki? If
	 *  not called the default is false.
	 */
	public function getLimitSearchToLocalWiki() {
		return $this->limitSearchToLocalWiki;
	}

	/**
	 * @param bool $localWikiOnly Should this search limit results to the local wiki? If
	 *  not called the default is false.
	 */
	public function setLimitSearchToLocalWiki( $localWikiOnly ) {
		if ( $localWikiOnly !== $this->limitSearchToLocalWiki ) {
			$this->isDirty = true;
			$this->limitSearchToLocalWiki = $localWikiOnly;
		}
	}

	/**
	 * @return int The number of seconds to cache results for
	 */
	public function getCacheTtl() {
		return $this->cacheTtl;
	}

	/**
	 * @param int $ttl The number of seconds to cache results for
	 */
	public function setCacheTtl( $ttl ) {
		$this->isDirty = true;
		$this->cacheTtl = $ttl;
	}

	/**
	 * @return string the original search term
	 */
	public function getOriginalSearchTerm() {
		return $this->originalSearchTerm;
	}

	/**
	 * @param string $term
	 */
	public function setOriginalSearchTerm( $term ) {
		// Intentionally does not set dirty to true. This is used only
		// for logging, as of july 2017.
		$this->originalSearchTerm = $term;
	}

	/**
	 * @return string The search term with keywords removed
	 */
	public function getCleanedSearchTerm() {
		return $this->cleanedSearchTerm;
	}

	/**
	 * @param string $term The search term with keywords removed
	 */
	public function setCleanedSearchTerm( $term ) {
		$this->isDirty = true;
		$this->cleanedSearchTerm = $term;
	}

	/**
	 * @return Escaper
	 */
	public function escaper() {
		return $this->escaper;
	}

	/**
	 * @return BoostFunctionBuilder[]
	 */
	public function getExtraScoreBuilders() {
		return $this->extraScoreBuilders;
	}

	/**
	 * Add custom scoring function to the context.
	 * The rescore builder will pick it up.
	 */
	public function addCustomRescoreComponent( BoostFunctionBuilder $rescore ) {
		$this->isDirty = true;
		$this->extraScoreBuilders[] = $rescore;
	}

	/**
	 * @param string $message i18n message key
	 * @param mixed ...$params
	 */
	public function addWarning( $message, ...$params ) {
		$this->isDirty = true;
		$this->warnings[] = [ $message, ...array_filter( $params, static function ( $v ) {
			return $v !== null;
		} ) ];
	}

	/**
	 * @return array[] Array of arrays. Each sub array is a set of values
	 *  suitable for creating an i18n message.
	 * @phan-return non-empty-array[]
	 */
	public function getWarnings() {
		return $this->warnings;
	}

	/**
	 * @return string the name of the fulltext query builder profile
	 */
	public function getFulltextQueryBuilderProfile() {
		if ( $this->fulltextQueryBuilderProfile === null ) {
			$this->fulltextQueryBuilderProfile = $this->config->getProfileService()
				->getProfileName( SearchProfileService::FT_QUERY_BUILDER, $this->profileContext );
		}
		return $this->fulltextQueryBuilderProfile;
	}

	/**
	 * @param string $profile set the name of the fulltext query builder profile
	 */
	public function setFulltextQueryBuilderProfile( $profile ) {
		$this->isDirty = true;
		$this->fulltextQueryBuilderProfile = $profile;
	}

	/**
	 * @param ResultsType $resultsType results type to return
	 */
	public function setResultsType( $resultsType ) {
		$this->resultsType = $resultsType;
	}

	/**
	 * @return ResultsType
	 */
	public function getResultsType() {
		Assert::precondition( $this->resultsType !== null, "resultsType unset" );
		return $this->resultsType;
	}

	/**
	 * Get the list of extra indices to query.
	 * Generally needed to query externilized file index.
	 * Must be called only once the list of namespaces has been set.
	 *
	 * @return ExternalIndex[]
	 * @see OtherIndexesUpdater::getExtraIndexesForNamespaces()
	 */
	public function getExtraIndices() {
		if ( $this->getLimitSearchToLocalWiki() || !$this->getNamespaces() ) {
			return [];
		}
		return OtherIndexesUpdater::getExtraIndexesForNamespaces(
			$this->config,
			$this->getNamespaces()
		);
	}

	/**
	 * Get the phrase rescore query if available
	 * @return AbstractQuery|null
	 */
	public function getPhraseRescoreQuery() {
		return $this->phraseRescoreQuery;
	}

	/**
	 * @param AbstractQuery|null $phraseRescoreQuery
	 */
	public function setPhraseRescoreQuery( $phraseRescoreQuery ) {
		$this->phraseRescoreQuery = $phraseRescoreQuery;
		$this->isDirty = true;
	}

	/**
	 * Add aggregation to perform on search.
	 */
	public function addAggregation( AbstractAggregation $agg ) {
		$this->aggs[] = $agg;
		$this->isDirty = true;
	}

	/**
	 * Get the list of aggregations.
	 * @return AbstractAggregation[]
	 */
	public function getAggregations() {
		return $this->aggs;
	}

	/**
	 * @return CirrusDebugOptions
	 */
	public function getDebugOptions() {
		return $this->debugOptions;
	}

	/**
	 * NOTE: public for testing purposes.
	 * @return AbstractQuery[]
	 */
	public function getFilters(): array {
		return $this->filters;
	}

	public function must( AbstractQuery $query ) {
		$this->addFilter( $query );
	}

	public function mustNot( AbstractQuery $query ) {
		$this->addNotFilter( $query );
	}

	/**
	 * Builds a SearchContext based on a SearchQuery.
	 *
	 * Helper function used for building blocks that still work on top
	 * of the SearchContext+queryString instead of SearchQuery.
	 *
	 * States initialized:
	 *    - limitSearchToLocalWiki
	 *  - suggestion
	 *  - custom rescoreProfile/fulltextQueryBuilderProfile
	 *  - contextual filters: (eg. SearchEngine::$prefix)
	 *  - SuggestPrefix (DYM prefix: ~ and/or namespace header)
	 *
	 * @param SearchQuery $query
	 * @param FallbackRunner|null $fallbackRunner
	 * @param CirrusSearchHookRunner|null $cirrusSearchHookRunner
	 * @return self
	 * @throws \CirrusSearch\Parser\ParsedQueryClassifierException
	 */
	public static function fromSearchQuery(
		SearchQuery $query,
		?FallbackRunner $fallbackRunner = null,
		?CirrusSearchHookRunner $cirrusSearchHookRunner = null
	): self {
		$searchContext = new self(
			$query->getSearchConfig(),
			$query->getNamespaces(),
			$query->getDebugOptions(),
			$fallbackRunner,
			new FetchPhaseConfigBuilder(
				$query->getSearchConfig(),
				$query->getSearchEngineEntryPoint(),
				$query->shouldProvideAllSnippets()
			),
			$cirrusSearchHookRunner
		);

		$searchContext->searchQuery = $query;

		$searchContext->limitSearchToLocalWiki = !$query->getCrossSearchStrategy()->isExtraIndicesSearchSupported();

		$searchContext->rescoreProfile = $query->getForcedProfile( SearchProfileService::RESCORE );

		$profileContext = $query->getSearchConfig()
			->getProfileService()
			->getDispatchService()
			->bestRoute( $query )
			->getProfileContext();
		$searchContext->setProfileContext( $profileContext );
		$parsedQuery = $query->getParsedQuery();
		$basicQueryClasses = [
			BasicQueryClassifier::SIMPLE_BAG_OF_WORDS,
			BasicQueryClassifier::SIMPLE_PHRASE,
			BasicQueryClassifier::BAG_OF_WORDS_WITH_PHRASE,
			BasicQueryClassifier::COMPLEX_QUERY,
		];

		foreach ( $basicQueryClasses as $klass ) {
			if ( $parsedQuery->isQueryOfClass( $klass ) ) {
				$searchContext->syntaxUsed[$klass] = 1;
			}
		}
		// TODO: Clarify what happens when user forces a profile, should we disable the dispatch service?
		$searchContext->fulltextQueryBuilderProfile = $query->getForcedProfile( SearchProfileService::FT_QUERY_BUILDER );
		$searchContext->profileContextParams = $query->getProfileContextParameters();

		foreach ( $query->getContextualFilters() as $filter ) {
			$filter->populate( $searchContext );
		}
		$pQuery = $query->getParsedQuery();
		$searchContext->originalSearchTerm = $pQuery->getRawQuery();
		$searchContext->trackTotalHits = $query->mustTrackTotalHits();
		return $searchContext;
	}

	public function getFallbackRunner(): FallbackRunner {
		return $this->fallbackRunner;
	}

	public function disableFallbackRunner(): void {
		$this->fallbackRunner = FallbackRunner::noopRunner();
	}

	public function setTrackTotalHits( bool $trackTotalHits ): void {
		if ( $trackTotalHits !== $this->trackTotalHits ) {
			$this->isDirty = true;
			$this->trackTotalHits = $trackTotalHits;
		}
	}

	public function getTrackTotalHits(): bool {
		return $this->trackTotalHits;
	}
}
