<?php
/**
 * Normalize and match names between recipes, ingredients, and POS data.
 *
 * @package RestaurantCosting
 */

declare(strict_types=1);

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

/**
 * POS items often differ by casing or category; aliases disambiguate duplicates.
 */
final class RC_Name_Matcher {

	/**
	 * Normalize a name for comparison (lowercase, trim, collapse whitespace).
	 */
	public static function normalize( string $name ): string {
		$name = wp_strip_all_tags( $name );
		$name = html_entity_decode( $name, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
		$name = strtolower( trim( $name ) );
		$name = preg_replace( '/\s+/u', ' ', $name ) ?? $name;
		return $name;
	}

	/**
	 * Find ingredient by exact normalized name.
	 */
	public static function find_ingredient_id( string $name ): ?int {
		global $wpdb;

		$normalized = self::normalize( $name );
		if ( '' === $normalized ) {
			return null;
		}

		$table = RC_Database::table( 'ingredients' );
		$id    = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT id FROM {$table} WHERE name_normalized = %s LIMIT 1",
				$normalized
			)
		);

		return $id ? (int) $id : null;
	}

	/**
	 * Resolve POS line to menu item using alias + category + group.
	 *
	 * @param string      $menu_item_name POS menu item label.
	 * @param string|null $sales_category Optional disambiguator.
	 * @param string|null $menu_group     Optional disambiguator.
	 */
	public static function resolve_menu_item_id(
		string $menu_item_name,
		?string $sales_category = null,
		?string $menu_group = null
	): ?int {
		global $wpdb;

		$normalized = self::normalize( $menu_item_name );
		if ( '' === $normalized ) {
			return null;
		}

		$menu_table   = RC_Database::table( 'menu_items' );
		$alias_table  = RC_Database::table( 'menu_item_aliases' );
		$cat          = $sales_category ? self::normalize( $sales_category ) : null;
		$group        = $menu_group ? self::normalize( $menu_group ) : null;

		// 1) Alias with full scope (name + category + group).
		if ( null !== $cat && null !== $group ) {
			$id = $wpdb->get_var(
				$wpdb->prepare(
					"SELECT menu_item_id FROM {$alias_table}
					WHERE name_normalized = %s
					AND sales_category = %s AND menu_group = %s
					LIMIT 1",
					$normalized,
					$cat,
					$group
				)
			);
			if ( $id ) {
				return (int) $id;
			}
		}

		// 2) Alias with category only.
		if ( null !== $cat ) {
			$id = $wpdb->get_var(
				$wpdb->prepare(
					"SELECT menu_item_id FROM {$alias_table}
					WHERE name_normalized = %s AND sales_category = %s
					AND (menu_group IS NULL OR menu_group = '')
					LIMIT 1",
					$normalized,
					$cat
				)
			);
			if ( $id ) {
				return (int) $id;
			}
		}

		// 3) Global alias (no category/group).
		$id = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT menu_item_id FROM {$alias_table}
				WHERE name_normalized = %s
				AND (sales_category IS NULL OR sales_category = '')
				AND (menu_group IS NULL OR menu_group = '')
				LIMIT 1",
				$normalized
			)
		);
		if ( $id ) {
			return (int) $id;
		}

		// 4) Canonical menu item name.
		$id = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT id FROM {$menu_table} WHERE name_normalized = %s LIMIT 1",
				$normalized
			)
		);

		return $id ? (int) $id : null;
	}

	/**
	 * Register POS alias for a menu item (used when auto-linking imports).
	 */
	public static function add_menu_alias(
		int $menu_item_id,
		string $alias_name,
		?string $sales_category = null,
		?string $menu_group = null
	): bool {
		global $wpdb;

		$normalized = self::normalize( $alias_name );
		if ( '' === $normalized ) {
			return false;
		}

		$cat   = $sales_category ? self::normalize( $sales_category ) : null;
		$group = $menu_group ? self::normalize( $menu_group ) : null;

		$table = RC_Database::table( 'menu_item_aliases' );
		$wpdb->replace(
			$table,
			array(
				'menu_item_id'    => $menu_item_id,
				'alias_name'      => $alias_name,
				'name_normalized' => $normalized,
				'sales_category'  => $cat,
				'menu_group'      => $group,
			),
			array( '%d', '%s', '%s', '%s', '%s' )
		);
		RC_Exception::throw_on_db_error( 'replace', $table );

		return false !== $wpdb->insert_id || 0 < (int) $wpdb->rows_affected;
	}

	/**
	 * Suggest menu items for an unmatched POS name (LIKE search).
	 *
	 * @return array<int, array{id:int, name:string, score:float}>
	 */
	public static function suggest_menu_items( string $pos_name, int $limit = 5 ): array {
		global $wpdb;

		$normalized = self::normalize( $pos_name );
		$table      = RC_Database::table( 'menu_items' );
		$like       = '%' . $wpdb->esc_like( $normalized ) . '%';

		$rows = $wpdb->get_results(
			$wpdb->prepare(
				"SELECT id, name, name_normalized FROM {$table}
				WHERE name_normalized LIKE %s OR %s LIKE CONCAT('%%', name_normalized, '%%')
				ORDER BY CHAR_LENGTH(name_normalized) ASC
				LIMIT %d",
				$like,
				$normalized,
				$limit
			),
			ARRAY_A
		);

		$suggestions = array();
		foreach ( $rows ?: array() as $row ) {
			similar_text( $normalized, (string) $row['name_normalized'], $percent );
			$suggestions[] = array(
				'id'    => (int) $row['id'],
				'name'  => (string) $row['name'],
				'score' => round( $percent, 1 ),
			);
		}

		usort(
			$suggestions,
			static fn( array $a, array $b ): int => $b['score'] <=> $a['score']
		);

		return $suggestions;
	}
}
