<?php

namespace CelerSearch\Search;

defined( 'ABSPATH' ) || exit;

use CelerSearch\DataTransfer\IndexConfig;
use CelerSearch\Exceptions\MissingProviderException;
use CelerSearch\Exceptions\MissingServiceException;
use CelerSearch\Factories\IndexFactory;
use CelerSearch\Factories\SearchAreaFactory;
use CelerSearch\Indices\BaseIndex;
use CelerSearch\Interfaces\IRegistrable;
use CelerSearch\Repositories\IndexRepository;
use CelerSearch\Utilities\Logger;
use WP_Post;
use WP_Query;

class QueryIntegration implements IRegistrable {

	/**
	 * Current page hits with full data for highlighting
	 *
	 * @var array
	 */
	private array $current_page_hits = [];

	/**
	 * Total hits count
	 *
	 * @var int
	 */
	private int $total_hits = 0;

	/**
	 * Whether the current query was intercepted by CelerSearch
	 *
	 * Used by found_posts and posts_search filters to avoid re-evaluating
	 * should_intercept() after inject_results() modifies query vars.
	 *
	 * @var bool
	 */
	private bool $query_intercepted = false;

	/**
	 * Register hooks
	 *
	 * @return void
	 */
	public function register(): void {
		add_action( 'pre_get_posts', [ $this, 'maybe_intercept_query' ], 10 );
		add_action( 'loop_start', [ $this, 'begin_highlighting' ] );
		add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_highlighting_styles' ] );
	}

	/**
	 * Maybe intercept WordPress query to use CelerSearch
	 *
	 * @param WP_Query $query
	 *
	 * @return void
	 */
	public function maybe_intercept_query( WP_Query $query ) : void {
		$this->query_intercepted = false;
		$search_term = $query->get( 's' );

		// Check if this query should use CelerSearch
		if ( ! $this->should_intercept( $query ) ) {
			if ( $search_term ) {
				Logger::channel( Logger::CHANNEL_QUERIES )->debug( 'Query not intercepted', [
					'search_term'   => $search_term,
					'is_main_query' => $query->is_main_query(),
					'is_admin'      => is_admin(),
				] );
			}

			return;
		}

		// Get the appropriate index
		$index_config = $this->get_index_for_query( $query );

		if ( ! $index_config ) {
			Logger::channel( Logger::CHANNEL_QUERIES )->debug( 'No index found for query', [
				'search_term' => $search_term,
				'area_type'   => $this->determine_area_type( $query ),
			] );

			return; // No index found, let WP handle it
		}

		try {
			$start_time = microtime( true );

			// Create index instance from config ID
			$index = IndexFactory::create( $index_config->getId() );

			// Perform search
			$results = $this->perform_search( $query, $index );

			$duration_ms = round( ( microtime( true ) - $start_time ) * 1000, 2 );

			Logger::channel( Logger::CHANNEL_QUERIES )->info( 'Search executed', [
				'search_term'   => $search_term,
				'index_slug'    => $index->get_slug(),
				'results_count' => count( $results['hits'] ?? [] ),
				'total_hits'    => $results['estimatedTotalHits'] ?? 0,
				'duration_ms'   => $duration_ms,
				'area_type'     => $this->determine_area_type( $query ),
			] );

			// Modify query to return our results
			$this->inject_results( $query, $results );
		} catch ( \Exception $e ) {
			$settings = $this->get_settings();

			Logger::channel( Logger::CHANNEL_ERRORS )->error( 'Query integration failed', [
				'search_term'      => $search_term,
				'exception'        => $e->getMessage(),
				'index_id'         => $index_config->getId(),
				'fallback_enabled' => $settings['fallback_to_native'] ?? true,
			] );

			if ( ! isset( $settings['fallback_to_native'] ) || ! $settings['fallback_to_native'] ) {
				// If fallback is disabled, return no results
				add_filter( 'posts_pre_query', function () {
					return [];
				}, 10, 2 );
			}
		}
	}

	/**
	 * Check if query should be intercepted
	 *
	 * @param WP_Query $query
	 *
	 * @return bool
	 */
	private function should_intercept( WP_Query $query ): bool {
		$settings = $this->get_settings();

		// Check if CelerSearch is enabled globally
		if ( ! isset( $settings['enable_search'] ) || ! $settings['enable_search'] ) {
			return false;
		}

		// Check if explicitly disabled for this query (supports false, 0, '0')
		$celersearch = $query->get( 'celersearch' );
		if ( $celersearch === false || $celersearch === 0 || $celersearch === '0' ) {
			return false;
		}

		// Legacy fallback (undocumented)
		if ( $query->get( 'celersearch_disable' ) ) {
			return false;
		}

		// Check if there's a matching search area for this query context
		$area = $this->get_search_area_for_query( $query );
		if ( ! $area ) {
			// Must have search term if no area matches (original behavior)
			if ( ! $query->get( 's' ) ) {
				return false;
			}

			return false; // No matching search area found
		}

		// For non-browse areas, require a search term
		$area_type = $area['type'] ?? '';
		$is_browse_area = $this->is_browse_area( $area_type );
		if ( ! $is_browse_area && ! $query->get( 's' ) ) {
			return false;
		}

		return apply_filters( 'celersearch_should_intercept_query', true, $query );
	}

	/**
	 * Check if an area type is a browse area (doesn't require a search term)
	 *
	 * @param string $area_type
	 *
	 * @return bool
	 */
	private function is_browse_area( string $area_type ): bool {
		$browse_areas = [ 'woocommerce_shop_browse' ];

		return in_array( $area_type, apply_filters( 'celersearch_browse_area_types', $browse_areas ), true );
	}

	/**
	 * Check if the current context is a WooCommerce shop/archive page
	 *
	 * @param WP_Query $query
	 *
	 * @return bool
	 */
	private function is_woocommerce_shop_context( WP_Query $query ): bool {
		// These functions are only available after the main query is set up
		if ( ! did_action( 'wp' ) && ! doing_action( 'wp' ) ) {
			// During pre_get_posts, check query vars instead
			$post_type = $query->get( 'post_type' );
			$is_product_type = $post_type === 'product' || ( is_array( $post_type ) && in_array( 'product', $post_type, true ) );

			if ( ! $is_product_type ) {
				return false;
			}

			// Check for product taxonomy queries
			$product_cat = $query->get( 'product_cat' );
			$product_tag = $query->get( 'product_tag' );

			if ( ! empty( $product_cat ) || ! empty( $product_tag ) ) {
				return true;
			}

			// Check for WooCommerce shop page (post_type=product without search term)
			if ( $is_product_type && ! $query->get( 's' ) ) {
				return true;
			}

			return false;
		}

		// After wp action, WooCommerce conditional tags are available
		if ( function_exists( 'is_shop' ) && is_shop() ) {
			return true;
		}

		if ( function_exists( 'is_product_taxonomy' ) && is_product_taxonomy() ) {
			return true;
		}

		return false;
	}

	/**
	 * Get search area for the current query context
	 *
	 * @param WP_Query $query
	 *
	 * @return array|null
	 */
	private function get_search_area_for_query( WP_Query $query ): ?array {
		$settings = $this->get_settings();
		$areas    = $settings['search_areas'] ?? [];

		// Determine the search area type based on context
		$area_type = $this->determine_area_type( $query );

		// Find matching enabled search area
		foreach ( $areas as $area ) {
			if ( isset( $area['type'] ) && $area['type'] === $area_type && ! empty( $area['enabled'] ) ) {
				return $area;
			}
		}

		return null;
	}

	/**
	 * Determine the search area type based on query context
	 *
	 * @param WP_Query $query
	 *
	 * @return string|null
	 */
	private function determine_area_type( WP_Query $query ): ?string {
		// Check for admin context
		if ( is_admin() ) {
			// Use get_current_screen() for more reliable post type detection in admin
			$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;

			// Check for WooCommerce HPOS orders page first
			if ( $screen && class_exists( 'WooCommerce' ) ) {
				// HPOS: screen ID is 'woocommerce_page_wc-orders'
				if ( $screen->id === 'woocommerce_page_wc-orders' ) {
					return 'woocommerce_orders_search';
				}
				// Legacy: screen base is 'edit' with post_type 'shop_order'
				if ( $screen->base === 'edit' && $screen->post_type === 'shop_order' ) {
					return 'woocommerce_orders_search';
				}
			}

			if ( $screen && $screen->base === 'edit' ) {
				$post_type = $screen->post_type ?: 'post';

				if ( $post_type === 'attachment' ) {
					return 'admin_media_search';
				}

				$area_type = 'admin_' . $post_type . '_search';

				return apply_filters( 'celersearch_admin_area_type', $area_type, $query, $post_type );
			}

			// Fallback to query post_type if screen not available
			$post_type = $query->get( 'post_type' );

			// Normalize array post_type to string (use first element)
			if ( is_array( $post_type ) ) {
				$post_type = ! empty( $post_type ) ? reset( $post_type ) : 'post';
			}

			if ( $post_type === 'attachment' ) {
				return 'admin_media_search';
			}

			// Default to 'post' if empty
			if ( empty( $post_type ) ) {
				$post_type = 'post';
			}

			// Return per-post-type admin search area
			$area_type = 'admin_' . $post_type . '_search';

			// Allow custom admin search area types
			return apply_filters( 'celersearch_admin_area_type', $area_type, $query, $post_type );
		}

		// Check for REST API context
		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return 'rest_api_search';
		}

		// Frontend - check for WooCommerce contexts
		if ( class_exists( 'WooCommerce' ) ) {
			$post_type = $query->get( 'post_type' );
			$is_product_query = $post_type === 'product' || ( is_array( $post_type ) && in_array( 'product', $post_type, true ) );

			// Product search (has search term)
			if ( $is_product_query && $query->get( 's' ) ) {
				return 'woocommerce_product_search';
			}

			// Shop browse (no search term) — shop page, category/tag/attribute archives
			if ( $query->is_main_query() && $this->is_woocommerce_shop_context( $query ) ) {
				return 'woocommerce_shop_browse';
			}
		}

		// Frontend public search
		if ( $query->is_main_query() ) {
			return 'wordpress_public_search';
		}

		// Allow custom area type determination
		return apply_filters( 'celersearch_area_type', null, $query );
	}

	/**
	 * Get index configuration for query
	 *
	 * @param WP_Query $query
	 *
	 * @return IndexConfig|null
	 */
	private function get_index_for_query( WP_Query $query ) : ?IndexConfig {
		$repo = new IndexRepository();

		// Priority 1: Explicit slug parameter
		if ( $slug = $query->get( 'celersearch_index' ) ) {
			$index = $repo->find_by_slug( $slug );
			if ( $index ) {
				return $index;
			}
		}

		// Priority 2: Explicit ID parameter
		if ( $id = $query->get( 'celersearch_index_id' ) ) {
			try {
				$index = $repo->find( $id );
				if ( $index ) {
					Logger::channel( Logger::CHANNEL_QUERIES )->debug( 'Index selected by explicit ID', [
						'priority' => 2,
						'index_id' => $id,
					] );

					return $index;
				}
			} catch ( \Exception $e ) {
				Logger::channel( Logger::CHANNEL_QUERIES )->warning( 'Index ID not found', [
					'index_id' => $id,
				] );
			}
		}

		// Priority 3: Search area configuration
		$area = $this->get_search_area_for_query( $query );
		if ( $area && ! empty( $area['index_id'] ) ) {
			try {
				$index = $repo->find( $area['index_id'] );
				if ( $index ) {
					Logger::channel( Logger::CHANNEL_QUERIES )->debug( 'Index selected by search area', [
						'priority'   => 3,
						'area_type'  => $area['type'] ?? 'unknown',
						'index_id'   => $area['index_id'],
					] );

					return $index;
				} else {
					Logger::channel( Logger::CHANNEL_QUERIES )->warning( 'Index not found for search area', [
						'index_id'  => $area['index_id'],
						'area_type' => $area['type'] ?? 'unknown',
					] );
				}
			} catch ( \Exception $e ) {
				Logger::channel( Logger::CHANNEL_QUERIES )->warning( 'Search area index lookup failed', [
					'index_id'  => $area['index_id'],
					'exception' => $e->getMessage(),
				] );
			}
		}

		// Priority 4: Filter hook (allows custom logic)
		$index = apply_filters( 'celersearch_select_index', null, $query );

		// Priority 5: Default index
		if ( ! $index ) {
			$settings = $this->get_settings();
			if ( isset( $settings['default_index_id'] ) && $settings['default_index_id'] > 0 ) {
				try {
					$found_index = $repo->find( $settings['default_index_id'] );
					if ( $found_index ) {
						Logger::channel( Logger::CHANNEL_QUERIES )->debug( 'Index selected by default setting', [
							'priority' => 5,
							'index_id' => $settings['default_index_id'],
						] );
						$index = $found_index;
					}
				} catch ( \Exception $e ) {
					Logger::channel( Logger::CHANNEL_QUERIES )->warning( 'Default index not found', [
						'index_id'  => $settings['default_index_id'],
						'exception' => $e->getMessage(),
					] );
				}
			}
		}

		return $index;
	}


	/**
	 * Perform search using index
	 *
	 * @param WP_Query $query
	 * @param BaseIndex $index
	 *
	 * @return array
	 */
	private function perform_search( WP_Query $query, BaseIndex $index ): array {
		$search_term = $query->get( 's' );
		$per_page    = (int) $query->get( 'posts_per_page' );
		if ( $per_page <= 0 && $per_page !== -1 ) {
			$per_page = (int) get_option( 'posts_per_page', 10 );
		}
		$area_type   = $this->determine_area_type( $query );

		// Check both 'paged' and 'page' query vars
		$paged = 1;
		if ( get_query_var( 'paged' ) ) {
			$paged = (int) get_query_var( 'paged' );
		} elseif ( get_query_var( 'page' ) ) {
			$paged = (int) get_query_var( 'page' );
		}

		if ( $per_page === - 1 ) {
			$per_page = 1000; // Set a reasonable limit
		}

		// For browse areas, use empty string as query (returns all documents)
		$is_browse = $this->is_browse_area( $area_type ?? '' );
		$effective_search_term = $is_browse && empty( $search_term ) ? '' : $search_term;

		// Build search parameters with highlighting support
		$params = [
			'q'      => $effective_search_term,
			'limit'  => $per_page,
			'offset' => ( $paged - 1 ) * $per_page,
			'highlighting' => [
				'enabled' => ! empty( $effective_search_term ),
				'fields' => [ 'post_title', 'content' ],
				'pre_tag' => '<em class="celersearch-highlight">',
				'post_tag' => '</em>',
			],
		];

		// Build filters based on context
		if ( $is_browse ) {
			// For browse areas, use only the status filter and WooCommerce-specific filters.
			// Skip build_filters_from_query() because WooCommerce injects tax_query vars
			// that map to field paths (e.g., taxonomies.product_cat.slug) which don't exist
			// in the index — taxonomies are indexed as flat name arrays.
			$filters = [];

			$status_filter = $this->get_status_filter( $query );
			if ( ! empty( $status_filter ) ) {
				$filters[] = $status_filter;
			}

			$wc_filters = $this->build_woocommerce_browse_filters( $query );
			$filters    = array_merge( $filters, $wc_filters );

			$sort = $this->get_woocommerce_sort( $query );
			if ( ! empty( $sort ) ) {
				$params['sort'] = $sort;
			}
		} else {
			// For search queries, use the generic WP_Query filter builder
			$filters = $this->build_filters_from_query( $query );
		}

		if ( ! empty( $filters ) ) {
			$params['filters'] = $filters;
		}

		// Apply default sort from search area (only when sort isn't already set)
		if ( empty( $params['sort'] ) ) {
			$area = $this->get_search_area_for_query( $query );
			if ( ! empty( $area['default_sort'] ) ) {
				$sort_rules = $index->resolve_sort( $area['default_sort'] );
				if ( ! empty( $sort_rules ) ) {
					$params['sort'] = $sort_rules;
				}
			}
		}

		// Allow modification of search parameters
		$params = apply_filters( 'celersearch_search_params', $params, $query, $index );

		// Perform search
		try {
			$response = $index->search( $search_term, $params );

			// Convert SearchResponse object to array format for backward compatibility
			if ( method_exists( $response, 'get_hits' ) ) {
				$results = [
					'hits' => $response->get_hits(),
					'estimatedTotalHits' => $response->get_total_hits(),
					'totalPages' => $response->get_total_pages(),
					'hitsPerPage' => $response->get_hits_per_page(),
					'page' => $response->get_current_page(),
				];
			} else {
				$results = [];
			}
		} catch (\Exception $e) {
			Logger::channel( Logger::CHANNEL_ERRORS )->error( 'Search error in perform_search', [
				'search_term' => $search_term,
				'index_slug'  => $index->get_slug(),
				'exception'   => $e->getMessage(),
			] );
			$results = [];
		}

		// Allow modification of results
		return apply_filters( 'celersearch_search_results', $results, $query, $index );
	}

	/**
	 * Build service-agnostic filters from WP_Query
	 *
	 * @param WP_Query $query
	 *
	 * @return array
	 */
	private function build_filters_from_query( WP_Query $query ): array {
		$filters = [];

		// Handle post status filtering
		$status_filter = $this->get_status_filter( $query );
		if ( ! empty( $status_filter ) ) {
			$filters[] = $status_filter;
		}

		// Handle tax_query
		$tax_query = $query->get( 'tax_query' );
		if ( ! empty( $tax_query ) && is_array( $tax_query ) ) {
			$normalized = $this->normalize_tax_query( $tax_query );
			if ( ! empty( $normalized ) ) {
				$filters[] = $normalized;
			}
		}

		// Handle category_name
		$category = $query->get( 'category_name' );
		if ( ! empty( $category ) ) {
			$filters[] = [
				'field' => 'taxonomies.category.slug',
				'operator' => '=',
				'value' => $category,
			];
		}

		// Handle category ID (cat parameter)
		$cat = $query->get( 'cat' );
		if ( ! empty( $cat ) ) {
			$filters[] = [
				'field' => 'taxonomies.category.term_id',
				'operator' => '=',
				'value' => (int) $cat,
			];
		}

		// Handle tag
		$tag = $query->get( 'tag' );
		if ( ! empty( $tag ) ) {
			$filters[] = [
				'field' => 'taxonomies.post_tag.slug',
				'operator' => '=',
				'value' => $tag,
			];
		}

		return $filters;
	}

	/**
	 * Get status filter based on search context
	 *
	 * Frontend searches default to 'publish' only.
	 * Admin searches can see more statuses.
	 *
	 * @param WP_Query $query
	 *
	 * @return array|null
	 */
	private function get_status_filter( WP_Query $query ): ?array {
		$area_type = $this->determine_area_type( $query );

		// Determine allowed statuses based on context
		// Check if it's an admin area type (starts with 'admin_')
		$is_admin_area = is_admin() || ( $area_type && strpos( $area_type, 'admin_' ) === 0 );

		if ( $is_admin_area ) {
			// Admin context: allow searching drafts, pending, private, etc.
			$statuses = [ 'publish', 'draft', 'pending', 'private', 'future' ];
		} else {
			// Frontend/REST: only published posts
			$statuses = [ 'publish' ];
		}

		// Allow filtering the allowed statuses
		$statuses = apply_filters( 'celersearch_search_statuses', $statuses, $query, $area_type );

		// If empty or contains all possible statuses, don't filter
		if ( empty( $statuses ) ) {
			return null;
		}

		// Build the filter
		if ( count( $statuses ) === 1 ) {
			return [
				'field'    => 'status',
				'operator' => '=',
				'value'    => $statuses[0],
			];
		}

		return [
			'field'    => 'status',
			'operator' => 'IN',
			'value'    => $statuses,
		];
	}

	/**
	 * Normalize WordPress tax_query to service-agnostic format
	 *
	 * @param array $tax_query
	 *
	 * @return array
	 */
	private function normalize_tax_query( array $tax_query ): array {
		$relation = isset( $tax_query['relation'] ) ? strtoupper( $tax_query['relation'] ) : 'AND';
		$conditions = [];

		foreach ( $tax_query as $key => $tax ) {
			// Skip the relation key
			if ( $key === 'relation' || ! is_array( $tax ) ) {
				continue;
			}

			// Check if this is a nested tax_query
			if ( isset( $tax['relation'] ) ) {
				$nested = $this->normalize_tax_query( $tax );
				if ( ! empty( $nested ) ) {
					$conditions[] = $nested;
				}
				continue;
			}

			$taxonomy = isset( $tax['taxonomy'] ) ? $tax['taxonomy'] : '';
			$field = isset( $tax['field'] ) ? $tax['field'] : 'term_id';
			$terms = isset( $tax['terms'] ) ? (array) $tax['terms'] : [];
			$operator = isset( $tax['operator'] ) ? strtoupper( $tax['operator'] ) : 'IN';

			if ( empty( $taxonomy ) || empty( $terms ) ) {
				continue;
			}

			// Map WordPress field to our normalized field format
			$field_map = [
				'term_id' => 'taxonomies.' . $taxonomy . '.term_id',
				'slug' => 'taxonomies.' . $taxonomy . '.slug',
				'name' => 'taxonomies.' . $taxonomy . '.name',
			];

			$normalized_field = isset( $field_map[ $field ] ) ? $field_map[ $field ] : $field_map['term_id'];

			// Convert to normalized format based on operator
			switch ( $operator ) {
				case 'IN':
					if ( count( $terms ) === 1 ) {
						$conditions[] = [
							'field' => $normalized_field,
							'operator' => '=',
							'value' => $terms[0],
						];
					} else {
						$conditions[] = [
							'field' => $normalized_field,
							'operator' => 'IN',
							'value' => $terms,
						];
					}
					break;

				case 'NOT IN':
					if ( count( $terms ) === 1 ) {
						$conditions[] = [
							'field' => $normalized_field,
							'operator' => '!=',
							'value' => $terms[0],
						];
					} else {
						$conditions[] = [
							'field' => $normalized_field,
							'operator' => 'NOT IN',
							'value' => $terms,
						];
					}
					break;

				case 'AND':
					// All terms must match - create separate conditions with AND relation
					$and_conditions = [];
					foreach ( $terms as $term ) {
						$and_conditions[] = [
							'field' => $normalized_field,
							'operator' => '=',
							'value' => $term,
						];
					}
					if ( count( $and_conditions ) > 1 ) {
						$conditions[] = [
							'relation' => 'AND',
							'conditions' => $and_conditions,
						];
					} else {
						$conditions[] = $and_conditions[0];
					}
					break;
			}
		}

		if ( empty( $conditions ) ) {
			return [];
		}

		// If only one condition, return it directly
		if ( count( $conditions ) === 1 ) {
			return $conditions[0];
		}

		// Multiple conditions - wrap with relation
		return [
			'relation' => $relation,
			'conditions' => $conditions,
		];
	}

	/**
	 * Build filters from WooCommerce URL parameters for browse queries
	 *
	 * Parses standard WooCommerce filter params: min_price, max_price,
	 * rating_filter, and filter_{attribute} (layered nav widgets).
	 *
	 * @param WP_Query $query
	 *
	 * @return array Normalized filter conditions
	 */
	private function build_woocommerce_browse_filters( WP_Query $query ): array {
		$filters = [];

		// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only, matches WooCommerce core behavior

		// Price range filters
		if ( isset( $_GET['min_price'] ) ) {
			$filters[] = [
				'field'    => 'price',
				'operator' => '>=',
				'value'    => (float) sanitize_text_field( wp_unslash( $_GET['min_price'] ) ),
			];
		}

		if ( isset( $_GET['max_price'] ) ) {
			$filters[] = [
				'field'    => 'price',
				'operator' => '<=',
				'value'    => (float) sanitize_text_field( wp_unslash( $_GET['max_price'] ) ),
			];
		}

		// Rating filter (e.g., rating_filter=4,5 means 4-star and 5-star)
		if ( isset( $_GET['rating_filter'] ) ) {
			$ratings = array_map( 'intval', explode( ',', sanitize_text_field( wp_unslash( $_GET['rating_filter'] ) ) ) );
			$ratings = array_filter( $ratings, function ( $r ) {
				return $r >= 1 && $r <= 5;
			} );

			if ( ! empty( $ratings ) ) {
				$filters[] = [
					'field'    => 'ratings.average_rating',
					'operator' => 'IN',
					'value'    => $ratings,
				];
			}
		}

		// Stock filter
		if ( isset( $_GET['filter_stock_status'] ) ) {
			$stock = sanitize_text_field( wp_unslash( $_GET['filter_stock_status'] ) );
			if ( $stock === 'instock' ) {
				$filters[] = [
					'field'    => 'in_stock',
					'operator' => '=',
					'value'    => true,
				];
			}
		}

		// Attribute filters (filter_pa_color=blue,red, filter_pa_size=large)
		// URL values are slugs; taxonomy data is indexed as names
		foreach ( $_GET as $key => $value ) {
			if ( strpos( $key, 'filter_' ) !== 0 || $key === 'filter_stock_status' ) {
				continue;
			}

			$attribute = str_replace( 'filter_', '', sanitize_key( $key ) );
			$slugs     = array_map( 'sanitize_text_field', explode( ',', wp_unslash( $value ) ) );

			if ( empty( $slugs ) ) {
				continue;
			}

			// WooCommerce attribute taxonomies use the pa_ prefix
			$taxonomy = $attribute;
			if ( strpos( $taxonomy, 'pa_' ) !== 0 ) {
				$taxonomy = 'pa_' . $taxonomy;
			}

			// Resolve slugs to term names
			$term_names = [];
			foreach ( $slugs as $slug ) {
				$term = get_term_by( 'slug', $slug, $taxonomy );
				if ( ! $term || is_wp_error( $term ) ) {
					$term = get_term_by( 'name', $slug, $taxonomy );
				}
				if ( $term && ! is_wp_error( $term ) ) {
					$term_names[] = html_entity_decode( $term->name );
				}
			}

			if ( empty( $term_names ) ) {
				continue;
			}

			$filters[] = [
				'field'    => 'taxonomies.' . $taxonomy,
				'operator' => count( $term_names ) === 1 ? '=' : 'IN',
				'value'    => count( $term_names ) === 1 ? $term_names[0] : $term_names,
			];
		}

		// Product category filter: check _product_cat (filter UI param using || delimiter,
		// avoids WooCommerce query var hijack that causes 404), then fall back to product_cat
		// query var (WooCommerce category archive rewrites which use slugs).
		if ( isset( $_GET['_product_cat'] ) ) {
			$product_cat = sanitize_text_field( wp_unslash( $_GET['_product_cat'] ) );
		} else {
			$product_cat = $query->get( 'product_cat' );
		}
		if ( ! empty( $product_cat ) ) {
			$cat_values = strpos( $product_cat, '||' ) !== false
				? array_map( 'trim', explode( '||', $product_cat ) )
				: [ trim( $product_cat ) ];
			$cat_names  = [];
			foreach ( $cat_values as $cat_value ) {
				$term = get_term_by( 'slug', $cat_value, 'product_cat' );
				if ( ! $term || is_wp_error( $term ) ) {
					$term = get_term_by( 'name', $cat_value, 'product_cat' );
				}
				if ( $term && ! is_wp_error( $term ) ) {
					$cat_names[] = html_entity_decode( $term->name );
				}
			}
			if ( ! empty( $cat_names ) ) {
				$filters[] = [
					'field'    => 'taxonomies.product_cat',
					'operator' => count( $cat_names ) === 1 ? '=' : 'IN',
					'value'    => count( $cat_names ) === 1 ? $cat_names[0] : $cat_names,
				];
			}
		}

		if ( isset( $_GET['_product_tag'] ) ) {
			$product_tag = sanitize_text_field( wp_unslash( $_GET['_product_tag'] ) );
		} else {
			$product_tag = $query->get( 'product_tag' );
		}
		if ( ! empty( $product_tag ) ) {
			$tag_values = strpos( $product_tag, '||' ) !== false
				? array_map( 'trim', explode( '||', $product_tag ) )
				: [ trim( $product_tag ) ];
			$tag_names  = [];
			foreach ( $tag_values as $tag_value ) {
				$term = get_term_by( 'slug', $tag_value, 'product_tag' );
				if ( ! $term || is_wp_error( $term ) ) {
					$term = get_term_by( 'name', $tag_value, 'product_tag' );
				}
				if ( $term && ! is_wp_error( $term ) ) {
					$tag_names[] = html_entity_decode( $term->name );
				}
			}
			if ( ! empty( $tag_names ) ) {
				$filters[] = [
					'field'    => 'taxonomies.product_tag',
					'operator' => count( $tag_names ) === 1 ? '=' : 'IN',
					'value'    => count( $tag_names ) === 1 ? $tag_names[0] : $tag_names,
				];
			}
		}

		// phpcs:enable WordPress.Security.NonceVerification.Recommended

		return apply_filters( 'celersearch_woocommerce_browse_filters', $filters, $query );
	}

	/**
	 * Map WooCommerce orderby parameter to Meilisearch sort
	 *
	 * @param WP_Query $query
	 *
	 * @return array Sort rules in service format (e.g., ['price:asc'])
	 */
	private function get_woocommerce_sort( WP_Query $query ): array {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only check for sort parameter
		$orderby = isset( $_GET['orderby'] ) ? sanitize_text_field( wp_unslash( $_GET['orderby'] ) ) : '';

		if ( empty( $orderby ) ) {
			// WooCommerce default — use relevance (no explicit sort)
			return [];
		}

		$sort_map = [
			'popularity' => [ 'total_sales:desc' ],
			'rating'     => [ 'ratings.average_rating:desc' ],
			'date'       => [ 'post_date:desc' ],
			'price'      => [ 'price:asc' ],
			'price-desc' => [ 'price:desc' ],
		];

		$sort = $sort_map[ $orderby ] ?? [];

		return apply_filters( 'celersearch_woocommerce_sort', $sort, $orderby, $query );
	}

	/**
	 * Inject search results into query
	 *
	 * @param WP_Query $query
	 * @param array $results
	 *
	 * @return void
	 */
	private function inject_results( WP_Query $query, array $results ) : void {
		$this->query_intercepted = true;

		// Store hits for highlighting
		// Use post_id (available in all index types) with fallback to id (legacy PostsIndex)
		$this->current_page_hits = [];
		foreach ( $results['hits'] ?? [] as $hit ) {
			$post_id = $hit['post_id'] ?? $hit['id'] ?? null;
			if ( $post_id !== null ) {
				$this->current_page_hits[ (int) $post_id ] = $hit;
			}
		}

		// Extract post IDs from results
		$post_ids = array_keys( $this->current_page_hits );

		// Store total hits for pagination
		$this->total_hits = $results['estimatedTotalHits'] ?? count( $post_ids );

		// Make sure there are results by tricking WordPress into finding a non-existent post ID
		// Otherwise, the query returns all results
		if ( empty( $post_ids ) ) {
			$post_ids = [ 0 ];
		}

		// Determine post type for the modified query
		$area_type = $this->determine_area_type( $query );
		if ( $this->is_browse_area( $area_type ?? '' ) ) {
			// For browse queries, preserve the original post_type so WooCommerce
			// still recognizes this as a product archive and uses the shop template.
			$post_types = $query->get( 'post_type' );
		} else {
			// For search queries, use 'any' or explicit post_type from URL
			$post_types = 'any';
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only check for post type context
			$maybe_post_type = isset( $_GET['post_type'] ) ? sanitize_text_field( wp_unslash( $_GET['post_type'] ) ) : '';
			if ( ! empty( $maybe_post_type ) ) {
				$post_type_obj = get_post_type_object( $maybe_post_type );
				if ( null !== $post_type_obj ) {
					$post_types = $post_type_obj->name;
				}
			}
		}

		// Modify the query
		$query->set( 'post__in', $post_ids );
		$query->set( 'orderby', 'post__in' );
		$query->set( 'post_type', $post_types );
		$query->set( 'post_status', 'any' );
		$query->set( 'offset', 0 );
		// Save and clear search term (prevents WP native search SQL)
		$search_term = $query->get( 's' );
		$query->set( 's', '' );

		// Restore after SQL executes so get_search_query() works in templates
		// and begin_highlighting() can detect the intercepted query
		if ( $search_term !== '' ) {
			add_filter( 'the_posts', function ( $posts, $q ) use ( $query, $search_term ) {
				if ( $q === $query ) {
					$q->set( 's', $search_term );
				}
				return $posts;
			}, 10, 2 );
		}

		// Hook to modify found_posts for pagination
		add_filter( 'found_posts', [ $this, 'found_posts' ], 10, 2 );

		// Remove WordPress's native search SQL
		add_filter( 'posts_search', [ $this, 'posts_search' ], 10, 2 );
	}

	/**
	 * Return the actual number of results from search index
	 *
	 * @param int $found_posts
	 * @param WP_Query $query
	 *
	 * @return int
	 */
	public function found_posts( int $found_posts, WP_Query $query ) : int {
		if ( $this->query_intercepted ) {
			return $this->total_hits;
		}
		return $found_posts;
	}

	/**
	 * Remove WordPress native search SQL from WHERE clause
	 *
	 * @param string $search
	 * @param WP_Query $query
	 *
	 * @return string
	 */
	public function posts_search( string $search, WP_Query $query ) : string {
		if ( $this->query_intercepted ) {
			return '';
		}
		return $search;
	}

	/**
	 * Get CelerSearch settings
	 *
	 * @return array
	 */
	private function get_settings(): array {
		$defaults = [
			'enable_search'      => false,
			'default_index_id'   => 0,
			'search_areas'       => [],
			'fallback_to_native' => true,
		];

		$settings = get_option( 'celersearch_settings', [] );

		return wp_parse_args( $settings, $defaults );
	}

	/**
	 * Enqueue highlighting styles
	 *
	 * @return void
	 */
	public function enqueue_highlighting_styles() : void {
		if ( ! $this->highlighting_enabled() ) {
			return;
		}

		if ( ! apply_filters( 'celersearch_highlighting_enable_bundled_styles', true ) ) {
			return;
		}

		wp_enqueue_style(
			'celersearch-highlight',
			CELERSEARCH_PLUGIN_URL . 'assets/frontend/highlight.css',
			[],
			CELERSEARCH_PLUGIN_VERSION
		);
	}

	/**
	 * Begin highlighting on loop_start
	 *
	 * @param WP_Query $query
	 *
	 * @return void
	 */
	public function begin_highlighting( WP_Query $query ) : void {
		if ( ! $this->should_intercept( $query ) ) {
			return;
		}

		if ( ! $this->highlighting_enabled() ) {
			return;
		}

		add_filter( 'the_title', [ $this, 'highlight_the_title' ], 10, 2 );
		add_filter( 'get_the_excerpt', [ $this, 'highlight_get_the_excerpt' ], 10, 2 );

		add_action( 'loop_end', [ $this, 'end_highlighting' ] );
	}

	/**
	 * End highlighting on loop_end
	 *
	 * @param WP_Query $query
	 *
	 * @return void
	 */
	public function end_highlighting( WP_Query $query ) : void {
		remove_filter( 'the_title', [ $this, 'highlight_the_title' ], 10 );
		remove_filter( 'get_the_excerpt', [ $this, 'highlight_get_the_excerpt' ], 10 );

		remove_action( 'loop_end', [ $this, 'end_highlighting' ] );
	}

	/**
	 * Highlight the title
	 *
	 * @param string $title
	 * @param int $post_id
	 *
	 * @return string
	 */
	public function highlight_the_title( string $title, int $post_id ) : string {
		$highlighted_title = $this->current_page_hits[ $post_id ]['_formatted']['post_title'] ?? null;

		if ( ! empty( $highlighted_title ) ) {
			$title = $highlighted_title;
		}

		return $title;
	}

	/**
	 * Highlight the excerpt
	 *
	 * @param string $excerpt
	 * @param WP_Post $post
	 *
	 * @return string
	 */
	public function highlight_get_the_excerpt( string $excerpt, WP_Post $post ) : string {
		$highlighted_excerpt = $this->current_page_hits[ $post->ID ]['_formatted']['content'] ?? null;

		if ( ! empty( $highlighted_excerpt ) ) {
			$excerpt = $highlighted_excerpt;
		}

		return $excerpt;
	}

	/**
	 * Check if highlighting is enabled
	 *
	 * @return bool
	 */
	private function highlighting_enabled(): bool {
		return apply_filters( 'celersearch_highlighting_enabled', true );
	}
}
