<?php
/**
 * SQL LIKE prefix fast-path.
 *
 * Pure semantic search is bad at short partial inputs — typing "Adjustabl"
 * doesn't strongly map to "Adjustable Dumbbells Pair" because BGE embeds
 * the literal text, and the cosine sits just below the relevance threshold.
 *
 * For the autocomplete experience, shoppers expect classic prefix-match
 * behavior ("type what you see"). So before doing semantic, we run a
 * targeted SQL LIKE on product titles and surface those hits first.
 *
 * Strategy:
 *   1. Tokenize the query (split on whitespace, drop tokens < 2 chars).
 *   2. For each token, find products whose title starts with the token
 *      OR has a word starting with the token (`LIKE 'tok%' OR LIKE '% tok%'`).
 *   3. Score: title-start match > word-start match. Shorter title >
 *      longer title (more specific hit).
 *   4. Return up to $limit ranked product IDs.
 *
 * SQL uses `$wpdb->prepare` with `%s` placeholders. We escape `%` and `_`
 * in the user-supplied token via `$wpdb->esc_like` so wildcards aren't
 * smuggled in.
 *
 * @package FindAstra
 */

defined( 'ABSPATH' ) || exit;

final class FindAstra_Prefix_Search {

	/**
	 * @return int[] Ranked product IDs (best first).
	 */
	public function ids_for( string $query, int $limit = 8 ): array {
		$query = trim( $query );
		if ( '' === $query ) {
			return array();
		}

		$tokens = array_values( array_filter(
			preg_split( '/\s+/u', $query ) ?: array(),
			static fn( $t ) => mb_strlen( (string) $t ) >= 2
		) );
		if ( empty( $tokens ) ) {
			return array();
		}

		global $wpdb;

		// Build a single SQL pass that scores rows by:
		//   - title starts with token: 3 pts
		//   - any word in title starts with token: 2 pts
		//   - title contains token anywhere: 1 pt
		// Sum across all tokens, then order by score desc + title length asc.
		$cases  = array();
		$wheres = array();
		$args   = array();

		foreach ( $tokens as $tok ) {
			$like  = $wpdb->esc_like( $tok );
			$start = $like . '%';
			$word  = '% ' . $like . '%';
			$mid   = '%' . $like . '%';

			$cases[]  = "(CASE WHEN p.post_title LIKE %s THEN 3 WHEN p.post_title LIKE %s THEN 2 WHEN p.post_title LIKE %s THEN 1 ELSE 0 END)";
			$args[]   = $start;
			$args[]   = $word;
			$args[]   = $mid;
			$wheres[] = "p.post_title LIKE %s";
			$args[]   = $mid;
		}

		$score_expr = implode( ' + ', $cases );
		$where_expr = implode( ' OR ', $wheres );
		$args[]     = $limit;

		$table = $wpdb->posts;

		$sql = "SELECT p.ID AS id, ( {$score_expr} ) AS score
			FROM {$table} p
			WHERE p.post_type = 'product'
			  AND p.post_status = 'publish'
			  AND ( {$where_expr} )
			HAVING score > 0
			ORDER BY score DESC, CHAR_LENGTH(p.post_title) ASC
			LIMIT %d";

		// $sql interpolates only the trusted {$wpdb->posts} identifier and
		// generated %d/%s placeholders; every user value is bound through
		// prepare(). The prefix lexical match is computed live and not cached.
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$rows = $wpdb->get_results( $wpdb->prepare( $sql, ...$args ) );
		if ( empty( $rows ) ) {
			return array();
		}
		return array_values( array_map( static fn( $r ) => (int) $r->id, $rows ) );
	}
}
