<?php

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Shared published-product matcher for public frontend retrieval and cards.
 */
class Niwa_AI_Frontend_Product_Matcher {
	private const MAX_SCAN_LIMIT = 80;

	/**
	 * Find published products and rank exact model matches before fuzzy Woo hits.
	 *
	 * @param string $query Product search query.
	 * @param int    $limit Maximum products to return.
	 * @return array<int, WC_Product>
	 */
	public static function search_products( $query, $limit = 4 ) {
		if ( ! function_exists( 'wc_get_product' ) ) {
			return array();
		}

		$query      = trim( (string) $query );
		$limit      = max( 1, min( 20, intval( $limit ) ) );
		$scan_limit = min( self::MAX_SCAN_LIMIT, max( 24, $limit * 8 ) );
		$seen       = array();
		$products   = array();

		if ( '' === $query ) {
			return array();
		}

		if ( ctype_digit( $query ) ) {
			self::add_product_by_id( intval( $query ), $products, $seen );
		}

		if ( function_exists( 'wc_get_product_id_by_sku' ) ) {
			$sku_id = intval( wc_get_product_id_by_sku( $query ) );
			if ( $sku_id > 0 ) {
				self::add_product_by_id( $sku_id, $products, $seen );
			}
		}

		self::add_products_by_posts(
			get_posts(
				array(
					'post_type'      => 'product',
					'post_status'    => 'publish',
					'posts_per_page' => min( 10, $scan_limit ),
					'name'           => sanitize_title( $query ),
				)
			),
			$products,
			$seen
		);

		self::add_products_by_posts(
			get_posts(
				array(
					'post_type'      => 'product',
					'post_status'    => 'publish',
					'posts_per_page' => $scan_limit,
					's'              => $query,
				)
			),
			$products,
			$seen
		);

		self::add_products_by_ids( self::find_product_ids_by_title_phrase( $query, $scan_limit ), $products, $seen );
		self::add_products_by_ids( self::find_product_ids_by_title_tokens( $query, $scan_limit ), $products, $seen );
		self::add_products_by_ids( self::find_product_ids_by_sku_fragment( $query, $scan_limit ), $products, $seen );

		if ( function_exists( 'wc_get_products' ) ) {
			$woo_products = wc_get_products(
				array(
					'status' => 'publish',
					'limit'  => $scan_limit,
					'search' => $query,
				)
			);
			foreach ( (array) $woo_products as $product ) {
				self::add_product( $product, $products, $seen );
			}
			foreach ( (array) wc_get_products( array( 'status' => 'publish', 'limit' => $scan_limit ) ) as $product ) {
				self::add_product( $product, $products, $seen );
			}
		}

		$ranked = array();
		foreach ( $products as $product ) {
			$score = self::score_product_match( $query, $product );
			if ( $score < 25 ) {
				continue;
			}
			$ranked[] = array(
				'product' => $product,
				'score'   => $score,
			);
		}

		usort(
			$ranked,
			function ( $left, $right ) {
				$score_cmp = intval( $right['score'] ) <=> intval( $left['score'] );
				if ( 0 !== $score_cmp ) {
					return $score_cmp;
				}

				return strcasecmp( $left['product']->get_name(), $right['product']->get_name() );
			}
		);

		return array_map(
			function ( $row ) {
				return $row['product'];
			},
			array_slice( $ranked, 0, $limit )
		);
	}

	/**
	 * Expand a grounded product into catalog peers using Woo content similarity.
	 * No language-specific category or keyword rules are used.
	 *
	 * @param int[] $seed_ids Grounded WooCommerce product IDs.
	 * @param int   $limit    Maximum peers to return.
	 * @return array<int, WC_Product>
	 */
	public static function find_similar_products( $seed_ids, $limit = 4 ) {
		if ( ! function_exists( 'wc_get_products' ) ) {
			return array();
		}

		$limit    = max( 0, min( 20, intval( $limit ) ) );
		$seed_ids = array_values( array_filter( array_unique( array_map( 'intval', (array) $seed_ids ) ) ) );
		if ( 0 === $limit || empty( $seed_ids ) ) {
			return array();
		}

		$seeds = array_values(
			array_filter(
				array_map( 'wc_get_product', $seed_ids )
			)
		);
		if ( empty( $seeds ) ) {
			return array();
		}

		$rows = array();
		foreach ( (array) wc_get_products( array( 'status' => 'publish', 'limit' => self::MAX_SCAN_LIMIT ) ) as $candidate ) {
			if ( ! $candidate || in_array( intval( $candidate->get_id() ), $seed_ids, true ) ) {
				continue;
			}

			$similarity = 0.0;
			foreach ( $seeds as $seed ) {
				$similarity = max( $similarity, self::product_similarity( $seed, $candidate ) );
			}
			if ( $similarity < 0.2 ) {
				continue;
			}

			$rows[] = array(
				'product' => $candidate,
				'score'   => $similarity,
			);
		}

		usort(
			$rows,
			static function ( $left, $right ) {
				$score = floatval( $right['score'] ) <=> floatval( $left['score'] );
				return 0 !== $score ? $score : strcasecmp( $left['product']->get_name(), $right['product']->get_name() );
			}
		);

		return array_map(
			static function ( $row ) {
				return $row['product'];
			},
			array_slice( $rows, 0, $limit )
		);
	}

	private static function product_similarity( $left, $right ) {
		return self::token_similarity( self::product_search_text( $left ), self::product_search_text( $right ) );
	}

	private static function product_search_text( $product ) {
		if ( ! $product || ! method_exists( $product, 'get_id' ) ) {
			return '';
		}

		$parts = array(
			method_exists( $product, 'get_name' ) ? $product->get_name() : '',
			method_exists( $product, 'get_short_description' ) ? $product->get_short_description() : '',
			method_exists( $product, 'get_description' ) ? $product->get_description() : '',
		);
		if ( function_exists( 'wp_get_post_terms' ) ) {
			$parts = array_merge( $parts, (array) wp_get_post_terms( $product->get_id(), 'product_cat', array( 'fields' => 'names' ) ) );
			$parts = array_merge( $parts, (array) wp_get_post_terms( $product->get_id(), 'product_tag', array( 'fields' => 'names' ) ) );
		}
		if ( method_exists( $product, 'get_attributes' ) ) {
			foreach ( (array) $product->get_attributes() as $attribute ) {
				if ( is_object( $attribute ) ) {
					$name = method_exists( $attribute, 'get_name' ) ? (string) $attribute->get_name() : '';
					$parts[] = $name;
					if ( method_exists( $attribute, 'is_taxonomy' ) && $attribute->is_taxonomy() && function_exists( 'wc_get_product_terms' ) ) {
						$parts[] = implode( ' ', (array) wc_get_product_terms( intval( $product->get_id() ), $name, array( 'fields' => 'names' ) ) );
					} elseif ( method_exists( $attribute, 'get_options' ) ) {
						$parts[] = implode( ' ', (array) $attribute->get_options() );
					}
				}
			}
		}

		$text = implode( ' ', array_filter( array_map( 'strval', $parts ) ) );
		return function_exists( 'wp_strip_all_tags' ) ? wp_strip_all_tags( $text ) : strip_tags( $text );
	}

	private static function token_similarity( $left, $right ) {
		$left_tokens = array_values(
			array_filter(
				self::normalize_tokens( $left ),
				static function ( $token ) {
					return mb_strlen( $token ) >= 3 || preg_match( '/\p{N}/u', $token );
				}
			)
		);
		$right_tokens = array_values(
			array_filter(
				self::normalize_tokens( $right ),
				static function ( $token ) {
					return mb_strlen( $token ) >= 3 || preg_match( '/\p{N}/u', $token );
				}
			)
		);
		$overlap = count( array_intersect( $left_tokens, $right_tokens ) );
		if ( $overlap < 3 ) {
			return 0.0;
		}

		return ( 2 * $overlap ) / max( 1, count( $left_tokens ) + count( $right_tokens ) );
	}

	private static function add_products_by_posts( $posts, &$products, &$seen ) {
		foreach ( (array) $posts as $post ) {
			if ( ! empty( $post->ID ) ) {
				self::add_product_by_id( intval( $post->ID ), $products, $seen );
			}
		}
	}

	private static function add_products_by_ids( $ids, &$products, &$seen ) {
		foreach ( (array) $ids as $id ) {
			self::add_product_by_id( intval( $id ), $products, $seen );
		}
	}

	private static function add_product_by_id( $product_id, &$products, &$seen ) {
		if ( $product_id <= 0 || ! function_exists( 'wc_get_product' ) ) {
			return;
		}

		self::add_product( wc_get_product( $product_id ), $products, $seen );
	}

	private static function add_product( $product, &$products, &$seen ) {
		if ( ! $product || ! method_exists( $product, 'get_id' ) ) {
			return;
		}

		$product_id = intval( $product->get_id() );
		if ( $product_id <= 0 || isset( $seen[ $product_id ] ) ) {
			return;
		}

		if ( method_exists( $product, 'get_status' ) && 'publish' !== (string) $product->get_status() ) {
			return;
		}

		$seen[ $product_id ] = true;
		$products[]         = $product;
	}

	private static function find_product_ids_by_title_phrase( $query, $limit ) {
		global $wpdb;

		if ( ! $wpdb || '' === trim( (string) $query ) ) {
			return array();
		}

		$like = '%' . $wpdb->esc_like( trim( (string) $query ) ) . '%';

		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		return $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish' AND post_title LIKE %s ORDER BY post_title ASC LIMIT %d", $like, max( 1, intval( $limit ) ) ) );
	}

	private static function find_product_ids_by_title_tokens( $query, $limit ) {
		global $wpdb;

		if ( ! $wpdb ) {
			return array();
		}

		$tokens = self::normalize_tokens( $query );
		if ( empty( $tokens ) ) {
			return array();
		}

		$where  = array();
		$params = array();
		foreach ( $tokens as $token ) {
			$where[]  = 'post_title LIKE %s';
			$params[] = '%' . $wpdb->esc_like( $token ) . '%';
		}

		$params[] = max( 1, intval( $limit ) );
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$sql = "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'product' AND post_status = 'publish' AND " . implode( ' AND ', $where ) . ' ORDER BY post_title ASC LIMIT %d';

		return $wpdb->get_col( $wpdb->prepare( $sql, $params ) );
	}

	private static function find_product_ids_by_sku_fragment( $query, $limit ) {
		global $wpdb;

		if ( ! $wpdb || '' === trim( (string) $query ) ) {
			return array();
		}

		$like = '%' . $wpdb->esc_like( trim( (string) $query ) ) . '%';
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$sql = "SELECT post_id FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE p.post_type = 'product' AND p.post_status = 'publish' AND pm.meta_key = '_sku' AND pm.meta_value LIKE %s LIMIT %d";

		return $wpdb->get_col( $wpdb->prepare( $sql, $like, max( 1, intval( $limit ) ) ) );
	}

	private static function score_product_match( $query, $product ) {
		$query_lc = self::normalize_text( $query );
		$title    = self::normalize_text( method_exists( $product, 'get_name' ) ? $product->get_name() : '' );
		$sku      = self::normalize_text( method_exists( $product, 'get_sku' ) ? $product->get_sku() : '' );
		$slug     = self::normalize_text( get_post_field( 'post_name', intval( $product->get_id() ) ) );
		$score    = 0;

		if ( '' !== $query_lc && $query_lc === $title ) {
			$score += 260;
		} elseif ( '' !== $query_lc && false !== strpos( $title, $query_lc ) ) {
			$score += 180;
		}

		if ( '' !== $query_lc && $query_lc === $slug ) {
			$score += 180;
		} elseif ( '' !== $query_lc && false !== strpos( $slug, sanitize_title( $query_lc ) ) ) {
			$score += 100;
		}

		if ( '' !== $query_lc && $query_lc === $sku ) {
			$score += 220;
		} elseif ( '' !== $query_lc && '' !== $sku && false !== strpos( $sku, $query_lc ) ) {
			$score += 120;
		}

		$query_tokens = self::normalize_tokens( $query );
		$title_tokens = self::normalize_tokens( $title );
		$slug_tokens  = self::normalize_tokens( $slug );
		$sku_tokens   = self::normalize_tokens( $sku );
		$search_tokens = self::root_tokens( self::product_search_text( $product ) );
		$query_roots   = self::root_tokens( $query );

		if ( ! empty( $query_tokens ) ) {
			$title_overlap = count( array_intersect( $query_tokens, $title_tokens ) );
			$slug_overlap  = count( array_intersect( $query_tokens, $slug_tokens ) );
			$sku_overlap   = count( array_intersect( $query_tokens, $sku_tokens ) );

			$score += $title_overlap * 34;
			$score += $slug_overlap * 18;
			$score += $sku_overlap * 24;
			$search_overlap = count( array_intersect( $query_roots, $search_tokens ) );
			$score += $search_overlap * 28;

			if ( $title_overlap === count( $query_tokens ) ) {
				$score += 110;
			}
			if ( ! empty( $query_roots ) && $search_overlap === count( $query_roots ) ) {
				$score += 90;
			}
		}

		$query_numbers  = self::extract_numbers( $query );
		$product_numbers = self::extract_numbers( trim( $title . ' ' . $slug . ' ' . $sku ) );
		if ( ! empty( $query_numbers ) ) {
			$number_overlap = array_intersect( $query_numbers, $product_numbers );
			if ( ! empty( $number_overlap ) ) {
				$score += count( $number_overlap ) * 170;
			} elseif ( ! empty( $product_numbers ) ) {
				$score -= 320;
			}
		}

		return $score;
	}

	private static function normalize_text( $text ) {
		$text = trim( (string) $text );
		$text = function_exists( 'mb_strtolower' ) ? mb_strtolower( $text, 'UTF-8' ) : strtolower( $text );
		if ( function_exists( 'remove_accents' ) ) {
			$text = remove_accents( $text );
		}

		return $text;
	}

	private static function normalize_tokens( $text ) {
		$text = self::normalize_text( $text );
		if ( '' === $text ) {
			return array();
		}

		$text   = preg_replace( '/[^\p{L}\p{N}]+/u', ' ', $text );
		$tokens = preg_split( '/\s+/', trim( (string) $text ) );
		if ( ! is_array( $tokens ) ) {
			return array();
		}

		$normalized = array();
		foreach ( $tokens as $token ) {
			$token = trim( (string) $token );
			if ( '' === $token ) {
				continue;
			}
			$normalized[] = $token;
		}

		return array_values( array_unique( $normalized ) );
	}

	private static function root_tokens( $text ) {
		$roots = array();
		foreach ( self::normalize_tokens( $text ) as $token ) {
			$length = mb_strlen( $token );
			if ( $length >= 4 && ! preg_match( '/\p{N}/u', $token ) ) {
				$token = mb_substr( $token, 0, $length - 1 );
			}
			if ( mb_strlen( $token ) >= 3 ) {
				$roots[] = $token;
			}
		}
		return array_values( array_unique( $roots ) );
	}

	private static function extract_numbers( $text ) {
		$text = self::normalize_text( $text );
		if ( ! preg_match_all( '/\b\d+(?:[.,]\d+)?\b/', $text, $matches ) ) {
			return array();
		}

		$numbers = array();
		foreach ( (array) $matches[0] as $number ) {
			$value = floatval( str_replace( ',', '.', (string) $number ) );
			if ( $value <= 0 ) {
				continue;
			}
			if ( abs( $value - round( $value ) ) < 0.00001 ) {
				$numbers[] = (string) intval( round( $value ) );
			} else {
				$numbers[] = rtrim( rtrim( number_format( $value, 4, '.', '' ), '0' ), '.' );
			}
		}

		return array_values( array_unique( $numbers ) );
	}
}
