<?php
/**
 * SEO Meta Duplication Checker
 *
 * Detects duplicate/similar SEO meta content across published WooCommerce products.
 * Uses similar_text() for percentage-based matching with configurable thresholds.
 *
 * @package AIPT\Api\BulkGenerator\SeoMeta
 */

namespace AIPT\Api\BulkGenerator\SeoMeta;

use AIPT\Api\BulkGenerator\SeoMeta\Core\SeoPluginDetector;

/**
 * Class SeoMetaDuplicationChecker
 *
 * Compares generated meta titles/descriptions against existing products
 * and returns similar matches above configured thresholds.
 */
class SeoMetaDuplicationChecker {

	/**
	 * Similarity threshold for titles (percentage)
	 */
	const TITLE_THRESHOLD = 80;

	/**
	 * Similarity threshold for descriptions (percentage)
	 */
	const DESCRIPTION_THRESHOLD = 70;

	/**
	 * Maximum products to process per check
	 */
	const MAX_PRODUCTS_PER_CHECK = 500;

	/**
	 * Maximum similar products to return per item
	 */
	const MAX_RESULTS_PER_ITEM = 5;

	/**
	 * Maximum items to process per batch check
	 */
	private const MAX_ITEMS_PER_BATCH = 100;

	/**
	 * Maximum string length for similarity checks
	 */
	private const MAX_SIMILARITY_LENGTH = 500;

	/**
	 * Detected SEO plugin meta keys
	 *
	 * @var array
	 */
	private $meta_keys = [];

	/**
	 * Constructor
	 *
	 * Detects SEO plugin and caches meta keys.
	 */
	public function __construct() {
		$detected = SeoPluginDetector::detect();
		if ( ! is_array( $detected ) ) {
			$detected = [];
		}
		$this->meta_keys = $detected['meta_keys'] ?? [];
	}

	/**
	 * Check for duplicate/similar meta content for a single product
	 *
	 * Compares provided meta title and description against existing published products
	 * and returns top 5 most similar matches above configured thresholds.
	 *
	 * @param string $meta_title       The meta title to check.
	 * @param string $meta_description The meta description to check.
	 * @param int    $exclude_product_id Optional product ID to exclude from comparison.
	 *
	 * @return array Array of similar products with similarity scores.
	 */
	public function check_duplicates(
		string $meta_title,
		string $meta_description,
		int $exclude_product_id = 0
	): array {

		if ( empty( $meta_title ) && empty( $meta_description ) ) {
			return [];
		}

		if ( empty( $this->meta_keys ) ) {
			return [];
		}

		$results = [];

		$products = $this->get_published_products_with_meta();

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

		\update_post_meta_cache( $products );

		$title_key = $this->meta_keys['title_key'] ?? '';
		$description_key = $this->meta_keys['description_key'] ?? '';

		foreach ( $products as $product_id ) {
			$product_results = $this->compareProductMeta(
				(int) $product_id,
				$meta_title,
				$meta_description,
				$title_key,
				$description_key,
				$exclude_product_id
			);
			$results = array_merge( $results, $product_results );
		}

		usort( $results, function ( $a, $b ) {
			return $b['similarity_score'] <=> $a['similarity_score'];
		} );

		return array_slice( $results, 0, self::MAX_RESULTS_PER_ITEM );
	}

	/**
	 * Check for duplicates across multiple products in batch
	 *
	 * Efficiently processes multiple products and returns all duplicates found.
	 * Useful for bulk operations where multiple products need duplicate checking.
	 *
	 * @param array $items Array of items with 'product_id', 'title', 'description' keys.
	 *
	 * @return array Associative array with product_id as key and duplicate results as value.
	 */
	public function check_duplicates_batch( array $items ): array {
		if ( empty( $items ) ) {
			return [];
		}

		if ( count( $items ) > self::MAX_ITEMS_PER_BATCH ) {
			$items = array_slice( $items, 0, self::MAX_ITEMS_PER_BATCH );
		}

		if ( empty( $this->meta_keys ) ) {
			return [];
		}

		$products = $this->get_published_products_with_meta();
		if ( empty( $products ) ) {
			return [];
		}

		\update_post_meta_cache( $products );

		$batch_results = [];
		$title_key       = $this->meta_keys['title_key'] ?? '';
		$description_key = $this->meta_keys['description_key'] ?? '';

		foreach ( $items as $item ) {
			$exclude_product_id = $item['product_id'] ?? 0;
			$meta_title         = $item['title'] ?? '';
			$meta_description   = $item['description'] ?? '';

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

			if ( empty( $meta_title ) && empty( $meta_description ) ) {
				continue;
			}

			$results = [];

			foreach ( $products as $product_id ) {
				$product_results = $this->compareProductMeta(
					(int) $product_id,
					$meta_title,
					$meta_description,
					$title_key,
					$description_key,
					$exclude_product_id
				);
				$results = array_merge( $results, $product_results );
			}

			if ( ! empty( $results ) ) {

				usort( $results, function ( $a, $b ) {
					return $b['similarity_score'] <=> $a['similarity_score'];
				} );

				$batch_results[ $exclude_product_id ] = array_slice( $results, 0, self::MAX_RESULTS_PER_ITEM );
			}
		}

		return $batch_results;
	}

	/**
	 * Compare product meta against existing values and collect similar matches
	 *
	 * Extracted helper to reduce duplication between check_duplicates and check_duplicates_batch.
	 * Compares provided meta title and description against a product's existing meta values.
	 *
	 * @param int    $product_id         The product ID to compare against.
	 * @param string $meta_title         The meta title to check.
	 * @param string $meta_description   The meta description to check.
	 * @param string $title_key          The meta key for titles.
	 * @param string $description_key    The meta key for descriptions.
	 * @param int|null $exclude_product_id Optional product ID to exclude from comparison.
	 *
	 * @return array Array of similar matches found for this product.
	 */
	private function compareProductMeta(
		int $product_id,
		string $meta_title,
		string $meta_description,
		string $title_key,
		string $description_key,
		?int $exclude_product_id = null
	): array {
		$results = [];

		if ( $exclude_product_id > 0 && (int) $product_id === $exclude_product_id ) {
			return $results;
		}

		$existing_title = get_post_meta( $product_id, $title_key, true );
		$existing_description = get_post_meta( $product_id, $description_key, true );

		if ( ! empty( $meta_title ) && ! empty( $existing_title ) ) {
			$title_similarity = $this->calculate_similarity( $meta_title, $existing_title );

			if ( $title_similarity >= self::TITLE_THRESHOLD ) {
				$results[] = [
					'product_id' => $product_id,
					'product_name' => get_the_title( $product_id ),
					'similarity_score' => round( $title_similarity, 1 ),
					'field' => 'title',
					'existing_value' => $existing_title,
				];
			}
		}

		if ( ! empty( $meta_description ) && ! empty( $existing_description ) ) {
			$description_similarity = $this->calculate_similarity( $meta_description, $existing_description );

			if ( $description_similarity >= self::DESCRIPTION_THRESHOLD ) {
				$results[] = [
					'product_id' => $product_id,
					'product_name' => get_the_title( $product_id ),
					'similarity_score' => round( $description_similarity, 1 ),
					'field' => 'description',
					'existing_value' => $existing_description,
				];
			}
		}

		return $results;
	}

	/**
	 * Calculate similarity percentage between two strings
	 *
	 * Uses PHP's similar_text() function with percentage mode.
	 * Normalizes strings before comparison for better accuracy.
	 *
	 * @param string $str1 First string to compare.
	 * @param string $str2 Second string to compare.
	 *
	 * @return float Similarity percentage (0-100).
	 */
	private function calculate_similarity( string $str1, string $str2 ): float {

		$has_mb = function_exists( 'mb_strtolower' );
		$str1 = $has_mb ? mb_strtolower( trim( $str1 ), 'UTF-8' ) : strtolower( trim( $str1 ) );
		$str2 = $has_mb ? mb_strtolower( trim( $str2 ), 'UTF-8' ) : strtolower( trim( $str2 ) );

		if ( $str1 === $str2 ) {
			return 100.0;
		}

		$len1 = $has_mb ? mb_strlen( $str1, 'UTF-8' ) : strlen( $str1 );
		$len2 = $has_mb ? mb_strlen( $str2, 'UTF-8' ) : strlen( $str2 );

		if ( $len1 > 0 && $len2 > 0 ) {
			$length_ratio = min( $len1, $len2 ) / max( $len1, $len2 );
			if ( $length_ratio < 0.5 ) {
				return $length_ratio * 100;
			}
		}

		if ( $len1 > self::MAX_SIMILARITY_LENGTH || $len2 > self::MAX_SIMILARITY_LENGTH ) {

			$trunc1 = $has_mb ? mb_substr( $str1, 0, self::MAX_SIMILARITY_LENGTH, 'UTF-8' ) : substr( $str1, 0, self::MAX_SIMILARITY_LENGTH );
			$trunc2 = $has_mb ? mb_substr( $str2, 0, self::MAX_SIMILARITY_LENGTH, 'UTF-8' ) : substr( $str2, 0, self::MAX_SIMILARITY_LENGTH );

			$similarity = 0.0;
			similar_text( $trunc1, $trunc2, $similarity );
			return $similarity;
		}

		$similarity = 0.0;
		similar_text( $str1, $str2, $similarity );

		return $similarity;
	}

	/**
	 * Get published products with existing meta values
	 *
	 * Queries published products that have non-empty meta values.
	 * Limited to MAX_PRODUCTS_PER_CHECK for performance.
	 *
	 * @return array Array of product IDs.
	 */
	private function get_published_products_with_meta(): array {
		$title_key = $this->meta_keys['title_key'] ?? '';
		$description_key = $this->meta_keys['description_key'] ?? '';

		if ( empty( $title_key ) || empty( $description_key ) ) {
			return [];
		}

		$products = get_posts( [
			'post_type' => 'product',
			'post_status' => 'publish',
			'posts_per_page' => self::MAX_PRODUCTS_PER_CHECK,
			'fields' => 'ids',
			'meta_query' => [
				'relation' => 'OR',
				[
					'key' => $title_key,
					'value' => '',
					'compare' => '!=',
				],
				[
					'key' => $description_key,
					'value' => '',
					'compare' => '!=',
				],
			],
		] );

		return array_unique( is_array( $products ) ? $products : [] );
	}
}
