<?php
/**
 * Index Consumer Service
 *
 * Read-only interface for consuming the sharded AI Store Index.
 * Used by internal features like Category Description Generator and Chatbot.
 *
 * @package AIPT\Api\StoreIndex
 * @since 2.5.0
 * @updated 2.5.0 Refactored to use sharded index only (legacy single-file removed)
 */

namespace AIPT\Api\StoreIndex;

/**
 * IndexConsumer class
 *
 * Provides read-only access to the AI Product Index for consumer features.
 * Uses ShardManager for intelligent shard selection and LRU caching.
 */
class IndexConsumer {

	/**
	 * ShardManager instance for sharded index access.
	 *
	 * @var ShardManager
	 */
	private $shard_manager;

	/**
	 * Profile ID.
	 *
	 * @var int
	 */
	private $profile_id;

	/**
	 * Constructor.
	 *
	 * @param int|null $profile_id Profile ID for the index to consume. If null/0, uses active profile.
	 */
	public function __construct( ?int $profile_id = null ) {
		if ( empty( $profile_id ) ) {

			$profile_manager = new ProfileManager();
			$active          = $profile_manager->getActive();

			if ( $active ) {
				$this->profile_id = (int) $active['id'];
			} else {

				$default          = $profile_manager->getDefault();
				$this->profile_id = $default ? (int) $default['id'] : 1;
			}
		} else {
			$this->profile_id = $profile_id;
		}
		$this->shard_manager = new ShardManager( $this->profile_id );
	}

	/**
	 * Check if index is available and valid.
	 *
	 * @return bool True if index can be consumed.
	 */
	public function isAvailable(): bool {
		return $this->shard_manager->isAvailable();
	}

	/**
	 * Get index metadata.
	 *
	 * @return array|null Meta section or null if unavailable.
	 */
	public function getMeta(): ?array {
		$map = $this->shard_manager->loadMap();
		return $map['meta'] ?? null;
	}

	/**
	 * Get all product IDs for a category.
	 *
	 * Uses intelligent shard selection for efficient retrieval.
	 *
	 * @param int $category_id WooCommerce category term ID.
	 * @return int[] Array of product IDs.
	 */
	public function getProductsByCategory( int $category_id ): array {
		$result = $this->shard_manager->getProductsByCategory( $category_id );
		return $result['product_ids'] ?? array();
	}

	/**
	 * Get AI profiles for specific product IDs.
	 *
	 * @param int[] $product_ids Array of product IDs.
	 * @return array<int, array> Map of product_id => AIProfile.
	 */
	public function getProductsByIds( array $product_ids ): array {
		return $this->shard_manager->getProductsByIds( $product_ids );
	}

	/**
	 * Filter products by price range.
	 *
	 * Loads all shards via LRU cache.
	 * Combine with category filter for better performance on large catalogs.
	 *
	 * @param float $min Minimum price (inclusive).
	 * @param float $max Maximum price (inclusive).
	 * @return int[] Matching product IDs.
	 */
	public function filterByPrice( float $min, float $max ): array {
		$products = $this->loadAllProducts();

		if ( empty( $products ) ) {
			return array();
		}

		$results = array();
		foreach ( $products as $id => $profile ) {
			$price = (float) ( $profile['price'] ?? 0 );
			if ( $price >= $min && $price <= $max ) {
				$results[] = (int) $id;
			}
		}

		return $results;
	}

	/**
	 * Filter products by stock status.
	 *
	 * @param string $status 'instock'|'outofstock'|'onbackorder'.
	 * @return int[] Matching product IDs.
	 */
	public function filterByStock( string $status ): array {
		$products = $this->loadAllProducts();

		if ( empty( $products ) ) {
			return array();
		}

		$results = array();
		foreach ( $products as $id => $profile ) {
			if ( ( $profile['stock'] ?? '' ) === $status ) {
				$results[] = (int) $id;
			}
		}

		return $results;
	}

	/**
	 * Filter products by attribute value.
	 *
	 * @param string $attribute Attribute slug (e.g., 'pa_color').
	 * @param string $value     Attribute value (e.g., 'Red').
	 * @return int[] Matching product IDs.
	 */
	public function filterByAttribute( string $attribute, string $value ): array {
		$products = $this->loadAllProducts();

		if ( empty( $products ) ) {
			return array();
		}

		$results = array();
		foreach ( $products as $id => $profile ) {
			$attributes = $profile['attributes'] ?? array();
			if ( isset( $attributes[ $attribute ] ) && stripos( $attributes[ $attribute ], $value ) !== false ) {
				$results[] = (int) $id;
			}
		}

		return $results;
	}

	/**
	 * Apply multiple filters with AND logic.
	 *
	 * @param array $filters Array of filter criteria:
	 *   - 'category' => int
	 *   - 'price_min' => float
	 *   - 'price_max' => float
	 *   - 'stock' => string
	 *   - 'attributes' => ['slug' => 'value', ...].
	 * @return int[] Matching product IDs (intersection of all filters).
	 */
	public function filterProducts( array $filters ): array {
		$products = $this->loadAllProducts();

		if ( empty( $products ) ) {
			return array();
		}

		if ( isset( $filters['category'] ) ) {
			$result_ids = $this->getProductsByCategory( (int) $filters['category'] );
		} else {
			$result_ids = array_map( 'intval', array_keys( $products ) );
		}

		if ( isset( $filters['price_min'] ) || isset( $filters['price_max'] ) ) {
			$min        = (float) ( $filters['price_min'] ?? 0 );
			$max        = (float) ( $filters['price_max'] ?? PHP_FLOAT_MAX );
			$price_ids  = $this->filterByPrice( $min, $max );
			$result_ids = array_values( array_intersect( $result_ids, $price_ids ) );
		}

		if ( ! empty( $filters['stock'] ) ) {
			$stock_ids  = $this->filterByStock( $filters['stock'] );
			$result_ids = array_values( array_intersect( $result_ids, $stock_ids ) );
		}

		if ( ! empty( $filters['attributes'] ) && is_array( $filters['attributes'] ) ) {
			foreach ( $filters['attributes'] as $attr_slug => $attr_value ) {
				$attr_ids   = $this->filterByAttribute( $attr_slug, $attr_value );
				$result_ids = array_values( array_intersect( $result_ids, $attr_ids ) );
			}
		}

		return $result_ids;
	}

	/**
	 * Get all product IDs in the index.
	 *
	 * @return int[] Array of all product IDs.
	 */
	public function getAllProductIds(): array {
		$products = $this->loadAllProducts();

		if ( empty( $products ) ) {
			return array();
		}

		return array_map( 'intval', array_keys( $products ) );
	}

	/**
	 * Get all category IDs in the index.
	 *
	 * @return int[] Array of all category IDs.
	 */
	public function getAllCategoryIds(): array {
		$map = $this->shard_manager->loadMap();

		if ( $map === null || empty( $map['categories'] ) ) {
			return array();
		}

		return array_map( 'intval', array_keys( $map['categories'] ) );
	}

	/**
	 * Load all products from all shards.
	 *
	 * This loads all shards via LRU cache.
	 * Use sparingly for large catalogs; prefer category-based queries.
	 *
	 * @return array Products array (product_id => profile).
	 */
	private function loadAllProducts(): array {
		$map = $this->shard_manager->loadMap();
		if ( $map === null ) {
			return array();
		}

		$shard_count = $map['meta']['total_shards'] ?? 0;
		if ( $shard_count === 0 ) {
			return array();
		}

		$shard_ids = range( 0, $shard_count - 1 );
		$merged    = $this->shard_manager->loadAndMergeShards( $shard_ids );

		return $merged['products'] ?? array();
	}

	/**
	 * Clear the ShardManager cache.
	 *
	 * Call this after index is rebuilt to ensure fresh data.
	 *
	 * @return void
	 */
	public function clearCache(): void {
		$this->shard_manager->clearCache();
	}

	/**
	 * Get the number of products in the index.
	 *
	 * @return int Product count.
	 */
	public function getProductCount(): int {
		$map = $this->shard_manager->loadMap();
		return $map['meta']['total_products'] ?? 0;
	}

	/**
	 * Get the number of categories in the index.
	 *
	 * @return int Category count.
	 */
	public function getCategoryCount(): int {
		$map = $this->shard_manager->loadMap();
		return $map['meta']['total_categories'] ?? 0;
	}

	/**
	 * Get the number of shards in the index.
	 *
	 * @return int Shard count.
	 */
	public function getShardCount(): int {
		return $this->shard_manager->getShardCount();
	}

	/**
	 * Get cache statistics for debugging.
	 *
	 * @return array Cache stats from ShardManager.
	 */
	public function getCacheStats(): array {
		return $this->shard_manager->getCacheStats();
	}

	/**
	 * Search products by keywords with relevance scoring.
	 *
	 * Uses intelligent shard selection based on query analysis
	 * to minimize I/O while maximizing relevance.
	 *
	 * @param string $query       Natural language query string.
	 * @param int    $max_results Maximum number of results to return.
	 * @return array Array of product data with product_id, sorted by relevance.
	 */
	public function searchByKeywords( string $query, int $max_results = 10 ): array {

		$query_lower = strtolower( trim( $query ) );
		if ( empty( $query_lower ) ) {
			return array();
		}

		$query_words = array_filter(
			explode( ' ', preg_replace( '/[^a-z0-9\s]/', '', $query_lower ) ),
			function ( $word ) {
				return strlen( $word ) >= 3;
			}
		);

		$shard_hints = $this->shard_manager->getShardsForQuery( $query_lower );
		$shard_ids   = $shard_hints['shard_ids'];
		$confidence  = $shard_hints['confidence'];

		if ( empty( $shard_ids ) || $confidence < 0.3 ) {
			$shard_ids = $this->shard_manager->getDefaultShards( 3 );
		}

		$max_shards = (int) apply_filters( 'aipt_max_search_shards', 5 );
		$shard_ids  = array_slice( $shard_ids, 0, $max_shards );

		$merged   = $this->shard_manager->loadAndMergeShards( $shard_ids );
		$products = $merged['products'] ?? array();

		if ( empty( $products ) ) {
			return array();
		}

		if ( empty( $query_words ) ) {
			return $this->getDefaultProducts( $products, $max_results );
		}

		return $this->scoreAndRankProducts( $products, $query_words, $query_lower, $max_results );
	}

	/**
	 * Score and rank products by relevance.
	 *
	 * @param array  $products    Products to score.
	 * @param array  $query_words Query words.
	 * @param string $query_lower Full query lowercase.
	 * @param int    $max_results Maximum results.
	 * @return array Ranked results.
	 */
	private function scoreAndRankProducts( array $products, array $query_words, string $query_lower, int $max_results ): array {
		$top_results = array();
		$min_score   = 0;

		foreach ( $products as $product_id => $profile ) {
			$score = $this->scoreProduct( $profile, $query_words, $query_lower );

			if ( $score <= 0 ) {
				continue;
			}

			if ( count( $top_results ) < $max_results ) {
				$top_results[ (int) $product_id ] = array(
					'score'   => $score,
					'profile' => $profile,
				);
				$min_score = min( $min_score ?: $score, $score );
			} elseif ( $score > $min_score ) {

				$min_score_value = PHP_INT_MAX;
				$min_id          = null;

				foreach ( $top_results as $pid => $data ) {
					if ( $data['score'] < $min_score_value ) {
						$min_score_value = $data['score'];
						$min_id          = $pid;
					}
				}

				if ( null !== $min_id ) {
					unset( $top_results[ $min_id ] );
				}

				$top_results[ (int) $product_id ] = array(
					'score'   => $score,
					'profile' => $profile,
				);

				$scores    = array_column( $top_results, 'score' );
				$min_score = ! empty( $scores ) ? min( $scores ) : 0;
			}
		}

		if ( empty( $top_results ) ) {
			return $this->getDefaultProducts( $products, $max_results );
		}

		uasort(
			$top_results,
			function ( $a, $b ) {
				return $b['score'] - $a['score'];
			}
		);

		$sorted = array();
		foreach ( $top_results as $product_id => $data ) {
			$sorted[] = array(
				'product_id' => $product_id,
				'score'      => $data['score'],
				'profile'    => $data['profile'],
			);
		}

		$count = count( $sorted );
		if ( $count > 1 ) {
			$i = 0;
			while ( $i < $count ) {
				$current_score = $sorted[ $i ]['score'];
				$tie_start     = $i;

				while ( $i < $count && $sorted[ $i ]['score'] === $current_score ) {
					++$i;
				}

				if ( $i - $tie_start > 1 ) {
					$segment = array_slice( $sorted, $tie_start, $i - $tie_start );
					shuffle( $segment );
					array_splice( $sorted, $tie_start, $i - $tie_start, $segment );
				}
			}
		}

		$results = array();
		foreach ( $sorted as $item ) {
			$results[] = array_merge(
				array( 'product_id' => $item['product_id'] ),
				$item['profile']
			);
		}

		return $results;
	}

	/**
	 * Calculate relevance score for a product.
	 *
	 * @param array  $profile     Product profile from index.
	 * @param array  $query_words Array of query words (lowercase, min 3 chars).
	 * @param string $query_lower Full query string in lowercase.
	 * @return int Relevance score (0 = no match).
	 */
	private function scoreProduct( array $profile, array $query_words, string $query_lower ): int {
		$score = 0;

		$title           = strtolower( $profile['title'] ?? '' );
		$description     = strtolower( $profile['short_description'] ?? '' );
		$sku             = strtolower( $profile['sku'] ?? '' );
		$attributes      = array_map( 'strtolower', array_values( $profile['attributes'] ?? array() ) );
		$attributes_text = implode( ' ', $attributes );

		foreach ( $query_words as $word ) {

			if ( strpos( $title, $word ) !== false ) {
				$score += 10;
			}

			if ( ! empty( $sku ) && strpos( $sku, $word ) !== false ) {
				$score += 8;
			}

			if ( strpos( $description, $word ) !== false ) {
				$score += 5;
			}

			if ( strpos( $attributes_text, $word ) !== false ) {
				$score += 3;
			}
		}

		$title_normalized = $this->stripSeparators( $title );
		$query_normalized = $this->stripSeparators( $query_lower );

		if ( ! empty( $query_normalized ) && ! empty( $title_normalized ) ) {
			if ( strpos( $title_normalized, $query_normalized ) !== false ) {
				$score += 15;
			} elseif ( strpos( $query_normalized, $title_normalized ) !== false ) {
				$score += 12;
			}
		}

		if ( ! empty( $sku ) && ! empty( $query_normalized ) ) {
			$sku_normalized = $this->stripSeparators( $sku );
			if ( ! empty( $sku_normalized ) && strpos( $sku_normalized, $query_normalized ) !== false ) {
				$score += 15;
			}
		}

		if ( ! empty( $query_lower ) && strpos( $title, $query_lower ) !== false ) {
			$score += 20;
		}

		if ( ( $profile['stock'] ?? '' ) === 'instock' ) {
			$score += 2;
		}

		return $score;
	}

	/**
	 * Strip common separators for product code matching.
	 *
	 * Removes spaces, hyphens, underscores, dots, and em/en dashes to enable
	 * matching queries like "HP301" against titles like "HP 301 XL" or "TN-2420".
	 *
	 * @param string $text Text to normalize.
	 * @return string Text with all separators removed.
	 */
	private function stripSeparators( string $text ): string {
		return str_replace(
			array( ' ', '-', '_', '.', "\xE2\x80\x93", "\xE2\x80\x94" ),
			'',
			$text
		);
	}

	/**
	 * Get default products when no search matches.
	 *
	 * Returns a randomized selection of in-stock products first, then any products up to limit.
	 * Shuffling provides variety so users don't see the same products on every query.
	 *
	 * @param array $products    Products array from index.
	 * @param int   $max_results Maximum results to return.
	 * @return array Formatted product results.
	 */
	private function getDefaultProducts( array $products, int $max_results ): array {
		$in_stock     = array();
		$out_of_stock = array();

		foreach ( $products as $product_id => $profile ) {
			$formatted = array_merge(
				array( 'product_id' => (int) $product_id ),
				$profile
			);

			if ( ( $profile['stock'] ?? '' ) === 'instock' ) {
				$in_stock[] = $formatted;
			} else {
				$out_of_stock[] = $formatted;
			}
		}

		shuffle( $in_stock );
		shuffle( $out_of_stock );

		$results = array_slice( $in_stock, 0, $max_results );

		$remaining = $max_results - count( $results );
		if ( $remaining > 0 && ! empty( $out_of_stock ) ) {
			$results = array_merge( $results, array_slice( $out_of_stock, 0, $remaining ) );
		}

		return $results;
	}
}
