<?php
/** @noinspection RegExpRedundantEscape */

namespace WPDRMS\ASP\Search;

use stdClass;
use WP_Post;
use WPDRMS\ASP\Misc\Priorities;
use WPDRMS\ASP\Utils\MB;
use WPDRMS\ASP\Utils\Str;

defined('ABSPATH') || die("You can't access this file directly.");

/**
 * Index Type Search
 *
 * @phpstan-import-type ResultObj from AbstractSearch
 */
class SearchIndex extends SearchPostTypes {
	/**
	 * @var ResultObj[] results from the index table
	 */
	protected array $raw_results = array();

	/**
	 * Index table stores ampersands normally, but the search keywords have the HTML version.
	 * This method reverts the HTML ampersands back to the original ones.
	 *
	 * @return void
	 */
	protected function revertAmpersandsToAnds() {
		$this->s   = str_replace('&amp;', '&', $this->s);
		$this->sr  = str_replace(';pma&', '&', $this->s);
		$this->_s  = array_map(
			function ( $s ) {
				return str_replace('&amp;', '&', $s);
			},
			$this->_s,
		);
		$this->_sr = array_map(
			function ( $s ) {
				return str_replace(';pma&', '&', $s);
			},
			$this->_sr,
		);
	}

	protected function doSearch(): void {
		global $wpdb;
		global $q_config;

		$current_blog_id = get_current_blog_id();
		$args            = &$this->args;

		// Check if there is anything left to look for
		if ( $args['_limit'] > 0 ) {
			$limit = $args['_limit'];
		} elseif ( $args['_ajax_search'] ) {
			$limit = $args['posts_limit'];
		} else {
			$limit = $args['posts_limit_override'];
		}
		if ( $limit <= 0 ) {
			return;
		}

		$sd = $args['_sd'] ?? array();

		$kw_logic             = $args['keyword_logic'];
		$q_config['language'] = $args['_qtranslate_lang'];

		$this->revertAmpersandsToAnds();

		$s  = $this->s; // full keyword
		$_s = $this->_s; // array of keywords

		$sr  = $this->sr; // full reversed keyword
		$_sr = $this->_sr; // array of reversed keywords

		$group_priority_select = $this->buildPgpQuery('asp_index.doc');
		$group_priority_select = $group_priority_select . ' AS group_priority,';

		/**
		 * Determine if the priorities table should be used or not.
		 */
		$priority_select = Priorities::count() > 0 ? '
		IFNULL((
			SELECT
			aspp.priority
			FROM ' . wd_asp()->db->table('priorities') . ' as aspp
			WHERE aspp.post_id = asp_index.doc AND aspp.blog_id = ' . get_current_blog_id() . '
		), 100)
		' : 100;

		// ------------------------- Statuses ----------------------------//
		// Removed - it is already considered at index generation
		// ---------------------------------------------------------------//

		// ----------------------- Gather Types --------------------------*/
		/**
		 * @TODO: Cross-check with selected custom post types on the index table options panel, as if they match,
		 * this query is not needed at all!
		 */
		$page_q = '';
		if ( !empty($args['_exclude_page_parent_child']) ) {
			$page_q = " AND ( IF(asp_index.post_type <> 'page', 1,
						EXISTS (
							SELECT ID FROM $wpdb->posts xxp WHERE
							 xxp.ID = asp_index.doc AND
							 xxp.post_parent NOT IN (" . implode( ',', $args['_exclude_page_parent_child']) . ') AND
							 xxp.ID NOT IN (' . implode(',', $args['_exclude_page_parent_child']) . ')
						)
					)
				)';
		}

		// If no post types selected, well then return
		if ( count($args['post_type']) < 1 && $page_q === '' ) {
			return;
		} else {
			$words      = implode("','", $args['post_type']);
			$post_types = "(asp_index.post_type IN ('$words') $page_q)";
		}
		/*---------------------------------------------------------------*/
		$post_fields_query = '';
		if ( !in_array('attachment', $args['post_type'], true) ) {
			$exc_fields = array_diff(array( 'title', 'content', 'excerpt' ), $args['post_fields']);
			if ( count($exc_fields) > 0 ) {
				$post_fields_arr   = array_map(
					function ( $field ) {
						return "asp_index.$field = 0";
					},
					$exc_fields
				);
				$post_fields_query = 'AND (' . implode(' AND ', $post_fields_arr) . ') ';
			}
		}

		// ------------------------ Categories/tags/taxonomies ----------------------
		$term_query = $this->buildTermQuery('asp_index.doc', 'asp_index.post_type');
		// ---------------------------------------------------------------------

		/*------------- Custom Fields with Custom selectors -------------*/
		if (
			count($args['post_type']) === 1 &&
			in_array('attachment', $args['post_type'], true) &&
			!$args['attachments_cf_filters']
		) {
			$cf_select = '(1)';
		} else {
			$cf_select = $this->buildCffQuery('asp_index.doc');
		}
		/*---------------------------------------------------------------*/

		/*------------------------ Include id's -------------------------*/
		if ( !empty($args['post_in']) ) {
			$include_posts = ' AND (asp_index.doc IN (' . ( is_array($args['post_in']) ? implode(',', $args['post_in']) : $args['post_in'] ) . '))';
		} else {
			$include_posts = '';
		}
		/*---------------------------------------------------------------*/

		/*------------------------ Exclude id's -------------------------*/
		if ( !empty($args['post_not_in']) ) {
			$exclude_posts = ' AND (asp_index.doc NOT IN (' . ( is_array($args['post_not_in']) ? implode(',', $args['post_not_in']) : $args['post_not_in'] ) . '))';
		} else {
			$exclude_posts = '';
		}
		if ( !empty($args['post_not_in2']) ) {
			$exclude_posts .= 'AND (asp_index.doc NOT IN (' . implode(',', $args['post_not_in2']) . '))';
		}
		/*---------------------------------------------------------------*/

		// ------------------------ Term JOIN --------------------------- //
		// No need, this should be indexed...
		// -------------------------------------------------------------- //

		/*------------------------- WPML filter -------------------------*/
		$wpml_query = '';
		if ( $args['_wpml_lang'] !== '' ) {
			global $sitepress;
			$site_lang_selected = false;

			if ( is_object($sitepress) && method_exists($sitepress, 'get_default_language') ) {
				$site_lang_selected = $sitepress->get_default_language() === $args['_wpml_lang'];
			}

			$wpml_query = "asp_index.lang = '" . Str::escape($args['_wpml_lang']) . "'";

			/**
			 * Imported or some custom post types might have missing translations for the site default language.
			 * If the user currently searches on the default language, empty translation string is allowed.
			 */
			if (
				$args->wpml_display_missing_translations === 'on_all_languages' ||
				( $args->wpml_display_missing_translations === 'on_main_language' && $site_lang_selected )
			) {
				$wpml_query .= " OR asp_index.lang = ''";
			}
			$wpml_query = ' AND (' . $wpml_query . ')';
		}
		/*---------------------------------------------------------------*/

		/*----------------------- POLYLANG filter -----------------------*/
		$polylang_query = '';
		if ( $args['_polylang_lang'] !== '' && $wpml_query === '' ) {
			$polylang_query = " AND (asp_index.lang = '" . Str::escape($args['_polylang_lang']) . "')";
		}
		/*---------------------------------------------------------------*/

		/*----------------------- Date filtering ------------------------*/
		$date_query       = '';
		$date_query_parts = $this->get_date_query_parts('ddpp');

		if ( count($date_query_parts) > 0 ) {
			$date_query = " AND EXISTS( SELECT 1 FROM $wpdb->posts as ddpp WHERE " . implode(' AND ', $date_query_parts) . ' AND ddpp.ID = asp_index.doc) ';
		}
		/*---------------------------------------------------------------*/

		/*---------------------- Blog switching? ------------------------*/
		$blog_query = '';
		if ( is_multisite() ) {
			if ( $args['_switch_on_preprocess'] ) {
				$blog_query = 'AND asp_index.blogid IN (' . implode(',', $args['_selected_blogs']) . ')';
			} else {
				$blog_query = 'AND asp_index.blogid = ' . $current_blog_id;
			}
		}
		/*---------------------------------------------------------------*/

		/*---------------------- Relevance Stored -----------------------*/
		$rel_val_title      = $sd['it_title_weight'] ?? 10;
		$rel_val_content    = $sd['it_content_weight'] ?? 8;
		$rel_val_excerpt    = $sd['it_excerpt_weight'] ?? 5;
		$rel_val_permalinks = $sd['it_terms_weight'] ?? 3;
		$rel_val_terms      = $sd['it_terms_weight'] ?? 3;
		$rel_val_cf         = $sd['it_cf_weight'] ?? 3;
		$rel_val_author     = $sd['it_author_weight'] ?? 2;
		/*---------------------------------------------------------------*/

		/*------------------- Post type based ordering ------------------*/
		$p_type_priority = '';
		if ( isset($sd['use_post_type_order']) && $sd['use_post_type_order'] ) {
			foreach ( $sd['post_type_order'] as $pk => $p_order ) {
				$p_type_priority .= "
				WHEN '$p_order' THEN $pk ";
			}
			if ( $p_type_priority !== '' ) {
				$p_type_priority = '
					CASE asp_index.post_type
				' . ' ' . $p_type_priority . '
					  ELSE 999
					END ';
			} else {
				$p_type_priority = '1';
			}
		} else {
			$p_type_priority = '1';
		}
		/*---------------------------------------------------------------*/

		/*---------------- Primary custom field ordering ----------------*/
		$custom_field_selectp = '1 ';
		if (
			strpos($args['post_primary_order'], 'customfp') !== false &&
			$args['_post_primary_order_metakey'] !== ''
		) {
			// @phpstan-ignore-next-line
			$custom_field_selectp = "(SELECT IF(meta_value IS NULL, 0, meta_value)
			FROM $wpdb->postmeta
			WHERE
				$wpdb->postmeta.meta_key='" . esc_sql($args['_post_primary_order_metakey']) . "' AND
				$wpdb->postmeta.post_id=asp_index.doc
			LIMIT 1
			) ";
		}
		/*---------------------------------------------------------------*/

		/*--------------- Secondary custom field ordering ---------------*/
		$custom_field_selects = '1 ';
		if (
			strpos($args['post_secondary_order'], 'customfs') !== false &&
			$args['_post_secondary_order_metakey'] !== ''
		) {
			// @phpstan-ignore-next-line
			$custom_field_selects = "(SELECT IF(meta_value IS NULL, 0, meta_value)
			FROM $wpdb->postmeta
			WHERE
				$wpdb->postmeta.meta_key='" . esc_sql($args['_post_secondary_order_metakey']) . "' AND
				$wpdb->postmeta.post_id=asp_index.doc
			LIMIT 1
			) ";
		}
		/*---------------------------------------------------------------*/

		/*--------------------- Post parent IDs -------------------------*/
		$post_parents_select = '';
		if ( count($args['post_parent']) > 0 ) {
			$post_parents_select = "AND EXISTS (
				SELECT 1 FROM $wpdb->posts
				WHERE
					$wpdb->posts.ID = asp_index.doc AND
					$wpdb->posts.post_parent IN (" . implode(',', $args['post_parent']) . ')
			) ';
		}
		/*---------------------------------------------------------------*/

		/*--------------------- Post parent IDs -------------------------*/
		$post_parents_exclude_select = '';
		if ( count($args['post_parent_exclude']) > 0 ) {
			$post_parents_exclude_select = "AND NOT EXISTS (
				SELECT 1 FROM $wpdb->posts
				WHERE
					$wpdb->posts.ID = asp_index.doc AND
					$wpdb->posts.post_parent IN (" . implode(',', $args['post_parent_exclude']) . ')
			) ';
		}
		/*---------------------------------------------------------------*/

		/*----------------------- Exclude USER id -----------------------*/
		$user_select = '0';
		$user_join   = '';
		$user_query  = '';
		if ( isset($args['post_user_filter']['include']) ) {
			if ( !in_array(-1, $args['post_user_filter']['include']) ) { // @phpcs:ignore
				$user_query = 'AND pj.post_author IN (' . implode(', ', $args['post_user_filter']['include']) . ')
				';
			}
		}
		if ( isset($args['post_user_filter']['exclude']) ) {
			if ( !in_array(-1, $args['post_user_filter']['exclude']) ) { // @phpcs:ignore
				$user_query = 'AND pj.post_author NOT IN (' . implode(', ', $args['post_user_filter']['exclude']) . ') ';
			} else {
				return;
			}
		}
		if ( $user_query !== '' ) {
			$user_select = 'pj.post_author';
			$user_join   = "LEFT JOIN $wpdb->posts pj ON pj.ID = asp_index.doc";
		}
		/*---------------------------------------------------------------*/

		/*-------------- Additional Query parts by Filters --------------*/
		/**
		 * Use these filters to add additional parts to the select, join or where
		 * parts of the search query.
		 */
		$add_select = apply_filters('asp_it_query_add_select', '', $args, $s, $_s);
		$add_join   = apply_filters('asp_it_query_add_join', '', $args, $s, $_s);
		$add_where  = apply_filters('asp_it_query_add_where', '', $args, $s, $_s);
		/*---------------------------------------------------------------*/

		/**
		 * This is the main query.
		 *
		 * The ttid field is a bit tricky as the term_taxonomy_id doesn't always equal term_id,
		 * so we need the LEFT JOINS :(
		 */
		$this->ordering['primary']   = $args['post_primary_order'];
		$this->ordering['secondary'] = $args['post_secondary_order'];

		$_primary_field                  = explode(' ', $this->ordering['primary']);
		$this->ordering['primary_field'] = $_primary_field[0];

		$this->query = "
		SELECT
			{args_fields}
			$add_select
			asp_index.doc as id,
			asp_index.blogid as `blogid`,
			'pagepost' as `content_type`,
			$priority_select as `priority`,
			$p_type_priority as `p_type_priority`,
			$user_select as `post_author`,
			$custom_field_selectp as `customfp`,
			$custom_field_selects as `customfs`,
			'' as `post_date`,
			'' as post_modified,
			'' as `date`,
			0 as menu_order,
			'' as `title`,
			asp_index.post_type as `post_type`,
			$group_priority_select
			(
				 asp_index.title * $rel_val_title * {rmod} +
				 asp_index.content * $rel_val_content * {rmod}  +
				 asp_index.excerpt * $rel_val_excerpt * {rmod}  +
				 asp_index.comment * $rel_val_terms * {rmod}  +
				 asp_index.link * $rel_val_permalinks * {rmod} +
				 asp_index.tag * $rel_val_terms * {rmod}  +
				 asp_index.customfield * $rel_val_cf * {rmod}  +
				 asp_index.author * $rel_val_author * {rmod}
			) as `relevance`
		FROM
			" . wd_asp()->db->table('index') . " as asp_index
			$user_join
			$add_join
			{args_join}
		WHERE
				({like_query})
			AND $post_types
			$blog_query
			$wpml_query
			$polylang_query
			$term_query
			$user_query
			AND $cf_select
			$exclude_posts
			$include_posts
			$post_parents_select
			$post_parents_exclude_select
			$date_query
			$post_fields_query
			$add_where
			{args_where}
			{group_by}
		LIMIT {limit}";

		// Place the argument query fields
		if ( isset($args['cpt_query']) && is_array($args['cpt_query']) ) {
			$_mod_q = $args['cpt_query'];
			foreach ( $_mod_q as &$qv ) {
				$qv = str_replace($wpdb->posts . '.ID', 'asp_index.doc', $qv);
			}
			$this->query = str_replace(
				array( '{args_fields}', '{args_join}', '{args_where}', '{args_orderby}' ),
				array( $_mod_q['fields'], $_mod_q['join'], $_mod_q['where'], $_mod_q['orderby'] ),
				$this->query
			);
		} else {
			$this->query = str_replace(
				array( '{args_fields}', '{args_join}', '{args_where}', '{args_orderby}' ),
				'',
				$this->query
			);
		}

		$queries     = array();
		$results_arr = array();

		// $words = $options['set_exactonly'] == 1 ? array($s) : $_s;
		$words = $_s;

		if ( empty($words) ) {
			$queries[] = str_replace(array( '{like_query}', '{rmod}', '{limit}', '{group_by}' ), array( '1', 1, $this->getPoolSize(), 'GROUP BY id' ), $this->query);
		} elseif ( $kw_logic === 'orex' ) {
			$rmod       = 1;
			$like_query = "(asp_index.term = '" . implode("' OR asp_index.term = '", $words) . "') AND asp_index.term_reverse <> ''";
			$queries[]  = str_replace(array( '{like_query}', '{rmod}', '{limit}', '{group_by}' ), array( $like_query, $rmod, $this->getPoolSize(), '' ), $this->query);
		} elseif ( $kw_logic === 'andex' ) {
			foreach ( $words as $wk => $word ) {
				$rmod = max(10 - ( $wk * 8 ), 1);

				$like_query = "asp_index.term = '$word' AND asp_index.term_reverse <> ''";
				$queries[]  = str_replace(array( '{like_query}', '{rmod}', '{limit}', '{group_by}' ), array( $like_query, $rmod, $this->getPoolSize($word), '' ), $this->query);
			}
		} else {
			foreach ( $words as $wk => $word ) {
				$rmod = max(10 - ( $wk * 8 ), 1);

				$like_query = "asp_index.term LIKE '" . $word . "%' AND asp_index.term_reverse <> ''";
				$queries[]  = str_replace(array( '{like_query}', '{rmod}', '{limit}', '{group_by}' ), array( $like_query, $rmod, $this->getPoolSize($word), '' ), $this->query);

				$like_query = "asp_index.term_reverse LIKE '" . ( $_sr[ $wk ] ?? $sr ) . "%' AND asp_index.term_reverse <> ''";
				$queries[]  = str_replace(array( '{like_query}', '{rmod}', '{limit}', '{group_by}' ), array( $like_query, intval($rmod / 2), $this->getPoolSize($word), '' ), $this->query);
			}
		}

		/*---------------------- Post CPT IDs ---------------------------*/
		if ( in_array('ids', $args['post_fields'], true) && $s !== '' ) {
			$queries['doc'] = str_replace(array( '{like_query}', '{rmod}', '{limit}', '{group_by}' ), array( "asp_index.doc LIKE '$s'", 1, $this->getPoolSize(), 'GROUP BY id' ), $this->query);
		}
		/*---------------------------------------------------------------*/

		/*----------------------- Improved title and custom field search query ------------------*/
		if (
			str_starts_with($args->post_primary_order, 'relevance') &&
			in_array('title', $args['post_fields'], true) && ( MB::strlen($s) > 0 || count($_s) === 0 )
		) {
			$rmod = 1000;

			// Re-calculate the limit to slice the results to the real size
			if ( $args['_limit'] > 0 ) {
				$limit = $args['_limit'];
			} elseif ( $args['_ajax_search'] ) {
				$limit = $args['posts_limit'];
			} else {
				$limit = $args['posts_limit_override'];
			}
			if ( !$args['_ajax_search'] || $args['_show_more_results'] ) {
				$limit = $limit * $this->remaining_limit_mod;
			}

			// Exact title query
			$single_delimiter     = count($_s) === 1 ? '___' : '';
			$title_query          = str_replace(
				array( '{like_query}', '{rmod}', '{limit}', '{group_by}', 'asp_index.doc as id' ),
				array( "(asp_index.term_reverse = '' AND asp_index.term LIKE '" . $s . $single_delimiter . "')", $rmod * 2, $this->getPoolSize(), '', 'DISTINCT asp_index.doc as id' ),
				$this->query
			);
			$results_arr['exact'] = $wpdb->get_results($title_query); // phpcs:ignore

			// We reached the required limit, reset the other queries, as we don't need them
			if ( count($results_arr['exact']) >= $limit ) {
				$queries = array();
			} else {
				// partial query on "OR" and "AND"
				if ( $kw_logic === 'or' || $kw_logic === 'and' ) {
					$title_query = str_replace(
						array( '{like_query}', '{rmod}', '{limit}', '{group_by}', 'asp_index.doc as id' ),
						array( "(asp_index.term LIKE '$s%')", $rmod, $this->getPoolSize(), '', 'DISTINCT asp_index.doc as id' ),
						$this->query
					);
				} else { // partial query (starting with) until the first keyword for OREX and ANDEX
					$title_query = str_replace(
						array( '{like_query}', '{rmod}', '{limit}', '{group_by}', 'asp_index.doc as id' ),
						array( "(asp_index.term LIKE '$s %')", $rmod, $this->getPoolSize(), '', 'DISTINCT asp_index.doc as id' ),
						$this->query
					);
				}

				$results_arr['starts_with'] = $wpdb->get_results($title_query); // phpcs:ignore

				// We reached the required limit, reset the other queries, as we don't need them
				if ( count($results_arr['starts_with']) >= $limit ) {
					$queries = array();
				}
			}
		}
		/*---------------------------------------------------------------*/
		if ( count($queries) > 0 ) {
			foreach ( $queries as $k => $query ) {
				$query             = apply_filters('asp_query_indextable', $query, $args, $args['_id'], $args['_ajax_search']);
				$results_arr[ $k ] = $wpdb->get_results($query); // phpcs:ignore
			}
		}

		// Merge results depending on the logic
		$results_arr = $this->mergeRawResults($results_arr, $kw_logic);

		// We need to save this array with keys, will need the values later.
		$this->raw_results = $results_arr;

		// Do primary ordering here, because the results will slice, and we need the correct ones on the top
		self::orderBy(
			$results_arr,
			array(
				'engine'                    => $args['engine'],
				'primary_ordering'          => $args['post_primary_order'],
				'primary_ordering_metatype' => $args['post_primary_order_metatype'],
				'secondary_ordering'        => $args['post_primary_order'], // PRIMARY ORDER ON PURPOSE!! -> Secondary does not apply when primary == secondary
			)
		);

		// Re-calculate the limit to slice the results to the real size
		if ( $args['_limit'] > 0 ) {
			$limit = $args['_limit'];
		} elseif ( $args['_ajax_search'] ) {
			$limit = $args['posts_limit'];
		} else {
			$limit = $args['posts_limit_override'];
		}

		$this->results_count = count($results_arr) > $limit * $this->remaining_limit_mod ? $limit * $this->remaining_limit_mod : count($results_arr);
		// For non-ajax search, results count needs to be limited to the maximum limit,
		// ...as nothing is parsed beyond that
		if ( !$args['_ajax_search'] && $this->results_count > $limit ) { // phpcs:ignore
			$this->results_count = $limit;
		}

		// Apply new limit, but perserve the keys
		$results_arr = array_slice($results_arr, $args['_call_num'] * $limit, $limit, true);

		$this->results = $results_arr;

		// Do some pre-processing
		$this->preProcessResults();

		$this->return_count = count($this->results);
	}


	/**
	 * Merges the initial results array, creating a union or intersection.
	 *
	 * The function also adds up the relevance values of the results object.
	 *
	 * @param array<int|string, array<ResultObj>> $results_arr
	 * @param "and"|"or"|"andex"|"orex"           $kw_logic keyword logic (and, or, andex, orex)
	 * @return array<ResultObj> results array
	 */
	protected function mergeRawResults( array $results_arr, string $kw_logic = 'or' ): array {
		// Store the improved title and cf results array temporarily
		// We want these items to be the part of results, no matter what
		$fixed_results = array();
		foreach ( array( 'doc', 'exact', 'starts_with' ) as $key ) {
			if ( isset($results_arr[ $key ]) ) {
				/**
				 * Don't use $fixed_results[$key]
				 * PHP 8.1< does not support array unpacking with non-numeric keys
				 * ...$fixed_results will fail if the keys are numeric
				 */
				$fixed_results[] = $results_arr[ $key ];
				unset($results_arr[ $key ]);
			}
		}

		/*
		 * When using the "and" logic, the $results_arr contains the results in [term, term_reverse]
		 * results format. These should not be intersected with each other, so this small code
		 * snippet here divides the results array by groups of 2, then it merges ever pair to one result.
		 * This way it turns into [term1, term1_reverse, term2 ...]  array to [term1 union term1_reversed, ...]
		 *
		 * This is only neccessary with the "and" logic. Others work fine.
		 */
		if ( $kw_logic === 'and' && $this->s !== '' ) {
			$new_ra = array();
			$i      = 0;
			$tmp_v  = array();
			foreach ( $results_arr as $_v ) {
				if ( $i & 1 ) {
					// odd, so merge the previous with the current
					$new_ra[] = array_merge($tmp_v, $_v);
				}
				$tmp_v = $_v;
				++$i;
			}
			$results_arr = $new_ra;
		}

		$final_results = array();

		foreach ( array_merge($results_arr, $fixed_results) as $results ) {
			foreach ( $results as $r ) {
				if ( isset($final_results[ $r->blogid . 'x' . $r->id ]) ) {
					$final_results[ $r->blogid . 'x' . $r->id ]->relevance += $r->relevance;
				} else {
					$final_results[ $r->blogid . 'x' . $r->id ] = $r;
				}
			}
		}

		if ( $kw_logic === 'or' || $kw_logic === 'orex' ) {
			return $final_results;
		}

		foreach ( $results_arr as $results ) {
			/**
			 * Why the array_merge($results, $title_results) ?
			 *  -> Because here is an AND or ANDEX logic, so array intersections will be returned.
			 *     All elements in the $title_results array not necessarily are a union of subset of each $results array.
			 *     To make sure that the elements of $title_results are indeed used, merge it with the actual $results
			 *     array. The $final_results at the end will contain all items from $title_results at all times.
			 */
			$final_results = array_uintersect(
				$final_results,
				array_merge($results, ...$fixed_results),
				array( $this, 'compareResults' )
			);
		}

		return $final_results;
	}

	public function getPoolSize( ?string $s = null ): int {
		$args = $this->args;
		$len  = $s === null ? 100 : MB::strlen($s);

		if ( $len <= 1 ) {
			$pool_size = $args['it_pool_size_one'];
		} elseif ( $len === 2 ) {
			$pool_size = $args['it_pool_size_two'];
		} elseif ( $len === 3 ) {
			$pool_size = $args['it_pool_size_three'];
		} else {
			$pool_size = $args['it_pool_size_rest'];
		}

		$pool_size = intval($pool_size);

		return max($pool_size, 100);
	}


	/**
	 * A custom comparison function for results intersection
	 *
	 * @param object{id: int, blogid: int} $a
	 * @param object{id: int, blogid: int} $b
	 *
	 * @return mixed
	 */
	protected function compareResults( object $a, object $b ) {
		if ( $a->blogid === $b->blogid ) {
			return $b->id - $a->id;
		}
		return $b->blogid - $a->blogid;
	}

	private function preProcessResults(): void {
		// No results, save some resources
		if ( count($this->results) === 0 ) {
			return;
		}

		$pageposts = array();
		$post_ids  = array();
		$the_posts = array();

		$args = $this->args;
		$sd   = $args['_sd'] ?? array();

		if ( $args['_ajax_search'] ) {
			$start = 0;
			$end   = count($this->results);
		} else {
			/**
			 * Offset =>
			 *  Positive -> Number of Index Table results displayed up until the current page
			 *  Negative -> (abs) Number of other result types displayed up until (and including) the current page
			 */
			$offset = ( ( $args['page'] - 1 ) * $args['posts_per_page'] ) - $args['global_found_posts'];
			$start  = max($offset, 0);
			$end    = $start + $args['posts_per_page'] + ( min($offset, 0) );
			$end    = $end > count($this->results) ? count($this->results) : $end;
		}

		$this->start_offset = $start;

		/**
		 * Do not use a for loop here, as the $this->results does not have numeric keys
		 */
		$k = 0;
		foreach ( $this->results as $r ) {
			if ( $k >= $start && $k < $end ) {
				$post_ids[ $r->blogid ][] = $r->id;
			}
			if ( $k >= $end ) {
				break;
			}
			++$k;
		}

		foreach ( $post_ids as $blogid => $the_ids ) {
			$this->switchMultisiteBlog(intval($blogid));

			$pargs = array(
				'post__in'       => $the_ids,
				// DO NOT use orderby=post__in, causes problems
				'posts_per_page' => -1,
				'post_status'    => 'any',
				// WARNING: Do NOT use "any" as post_type, it will not work!!!
				// @phpcs:ignore
				'post_type'      => !empty($args['_exclude_page_parent_child']) ?
					array_merge($args['post_type'], array( 'page' )) : $args['post_type'],
			);

			/**
			 * Polylang workaround
			 *  - Force any language, as the correct items are already returned by the index
			 *    table engine.
			 *  url: https://polylang.pro/doc/developpers-how-to/#all
			 */
			if ( function_exists('pll_the_languages') ) {
				$pargs['lang'] = '';
			}

			/**
			 * @var WP_Post[] $get_posts
			 */
			$get_posts = get_posts($pargs);
			foreach ( $get_posts as $gv ) {
				$gv->blogid = $blogid; // @phpstan-ignore-line
			}

			// Resort by ID, because orderby=post__in causes issues in some cases
			$sorted_get_posts = array();
			foreach ( $the_ids as $id ) {
				foreach ( $get_posts as $gp ) {
					if ( $gp->ID === intval($id) ) {
						$sorted_get_posts[] = $gp;
						break;
					}
				}
			}
			/**
			 * @var WP_Post[] $the_posts
			 */
			$the_posts = array_merge($the_posts, $sorted_get_posts);
		}

		$this->restoreMultisiteBlog();

		// Merge the posts with the raw results to a new array
		foreach ( $the_posts as $r ) {
			$new_result = new stdClass();

			$new_result->id         = $r->ID;
			$new_result->blogid     = $r->blogid; // @phpstan-ignore-line
			$new_result->title      = $r->post_title;
			$new_result->post_title = $new_result->title;
			$new_result->content    = $r->post_content;
			$new_result->excerpt    = $r->post_excerpt;
			$new_result->image      = null;

			if ( $sd['showauthor'] == 1 ) { // @phpcs:ignore
				$post_user = get_user_by('id', $r->post_author);

				if ( $post_user !== false ) {
					if ( $sd['author_field'] === 'display_name' ) {
						$new_result->author = $post_user->data->display_name;
					} else {
						$new_result->author = $post_user->data->user_login;
					}
				} else {
					$new_result->author = null;
				}
			}

			$new_result->date          = $r->post_date;
			$new_result->post_date     = $new_result->date;
			$new_result->post_modified = $r->post_modified;

			$new_result->menu_order = $r->menu_order;

			$key = $new_result->blogid . 'x' . $new_result->id;

			// Get the relevance and priority values
			$new_result->relevance       = (int) $this->raw_results[ $key ]->relevance;
			$new_result->priority        = (int) $this->raw_results[ $key ]->priority;
			$new_result->group_priority  = (int) $this->raw_results[ $key ]->group_priority;
			$new_result->p_type_priority = (int) $this->raw_results[ $key ]->p_type_priority;
			$new_result->post_type       = $this->raw_results[ $key ]->post_type;
			$new_result->average_rating  = floatval($this->raw_results[ $key ]->average_rating ?? 0);
			$new_result->customfp        = $this->raw_results[ $key ]->customfp ?? 1;
			$new_result->customfs        = $this->raw_results[ $key ]->customfs ?? 1;
			$new_result->content_type    = 'pagepost';
			$new_result->g_content_type  = 'post_page_cpt';

			$pageposts[] = $new_result;
		}

		self::orderBy(
			$pageposts,
			array(
				'engine'                      => $args['engine'],
				'primary_ordering'            => $args['post_primary_order'],
				'primary_ordering_metatype'   => $args['post_primary_order_metatype'],
				'secondary_ordering'          => $args['post_secondary_order'],
				'secondary_ordering_metatype' => $args['post_secondary_order_metatype'],
			)
		);

		$this->results = $pageposts;
	}
}
