<?php
/**
 * This file is part of the Wisewand WordPress plugin.
 *
 * @package Wisewand
 * @since 1.6.13
 * @license GPL-2.0-or-later
 */

namespace Wisewand\Utils;

// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use Wisewand\Logging\Logger;
use Wisewand\Repositories\Keyword_Repository;

/**
 * Semantic Deduplication Engine
 *
 * Uses OpenAI Embeddings (Vector Search) to find semantically similar keywords.
 * Replaces/Augments the legacy string-matching deduplication.
 */
class Semantic_Deduplication_Engine {

	private $logger;
	private $openai_client;
	private $repository;

	// Thresholds
	const THRESHOLD_DUPLICATE = 0.92; // Very high confidence (e.g. "Best shoes" vs "Top shoes")
	const THRESHOLD_RELATED   = 0.85; // High confidence (e.g. "Shoes" vs "Sneakers")

	public function __construct( Logger $logger, OpenAI_Client $openai_client, Keyword_Repository $repository ) {
		$this->logger        = $logger;
		$this->openai_client = $openai_client;
		$this->repository    = $repository;
	}

	/**
	 * Find semantic duplicates for a keyword
	 *
	 * @param string $keyword The keyword to check
	 * @param string $language_code Language code
	 * @return array List of duplicates with similarity scores
	 */
	public function find_duplicates( $keyword, $language_code = 'en' ) {
		// 1. Generate embedding for the new keyword
		$embedding = $this->openai_client->get_embeddings( $keyword );

		if ( is_wp_error( $embedding ) ) {
			$this->logger->error( 'Failed to generate embedding for deduplication', array( 'error' => $embedding->get_error_message() ) );
			return array();
		}

		// 2. Fetch candidates from DB
		// Optimization: We can't compare against ALL keywords in PHP (too slow).
		// We fetch "rough candidates" first (e.g. same language, maybe partial word match if possible, or recent 1000).
		// For now, we'll fetch all keywords with embeddings in the same language.
		// TODO: Implement "Hybrid Filter" if > 2000 keywords.
		$candidates = $this->repository->get_keywords_with_embeddings( $language_code );

		$matches = array();

		foreach ( $candidates as $candidate ) {
			if ( empty( $candidate->embedding ) ) {
				continue;
			}

			// Unpack binary blob to array
			$candidate_vector = maybe_unserialize( $candidate->embedding );
			if ( ! is_array( $candidate_vector ) ) {
				continue;
			}

			// 3. Calculate Cosine Similarity
			$score = $this->cosine_similarity( $embedding, $candidate_vector );

			if ( $score >= self::THRESHOLD_RELATED ) {
				$matches[] = array(
					'keyword'    => $candidate->keyword,
					'id'         => $candidate->id,
					'score'      => $score,
					'is_duplicate' => $score >= self::THRESHOLD_DUPLICATE,
				);
			}
		}

		// Sort by score DESC
		usort( $matches, function( $a, $b ) {
			return $b['score'] <=> $a['score'];
		} );

		return $matches;
	}

	/**
	 * Find semantic duplicates using a pre-calculated vector (Optimized for batching)
	 *
	 * @param array  $embedding_vector The 256-dim vector
	 * @param string $keyword The keyword text (for logging/reference)
	 * @param string $language_code Language code
	 * @return array List of duplicates
	 */
	public function find_duplicates_by_vector( $embedding_vector, $keyword, $language_code = 'en' ) {
		// Fetch candidates from DB
		// Optimization: We fetch all for the language. With 256 dims, 10k keywords = ~10MB RAM.
		// This is safe for now. If scaling > 50k, we need a limit or hybrid filter.
		$candidates = $this->repository->get_keywords_with_embeddings( $language_code );

		$matches = array();

		foreach ( $candidates as $candidate ) {
			if ( empty( $candidate->embedding ) ) {
				continue;
			}

			// Unpack binary blob to array
			$candidate_vector = maybe_unserialize( $candidate->embedding );
			if ( ! is_array( $candidate_vector ) ) {
				continue;
			}

			// Calculate Cosine Similarity
			$score = $this->cosine_similarity( $embedding_vector, $candidate_vector );

			if ( $score >= self::THRESHOLD_RELATED ) {
				$matches[] = array(
					'keyword'      => $candidate->keyword,
					'id'           => $candidate->id,
					'score'        => $score,
					'is_duplicate' => $score >= self::THRESHOLD_DUPLICATE,
				);
			}
		}

		// Sort by score DESC
		usort( $matches, function( $a, $b ) {
			return $b['score'] <=> $a['score'];
		} );

		return $matches;
	}

	/**
	 * Calculate Cosine Similarity between two vectors
	 *
	 * @param array $vec_a
	 * @param array $vec_b
	 * @return float Similarity score (-1 to 1)
	 */
	private function cosine_similarity( $vec_a, $vec_b ) {
		$dot_product = 0;
		$norm_a      = 0;
		$norm_b      = 0;

		foreach ( $vec_a as $key => $val ) {
			$dot_product += $val * $vec_b[ $key ];
			$norm_a      += $val * $val;
			$norm_b      += $vec_b[ $key ] * $vec_b[ $key ];
		}

		if ( $norm_a == 0 || $norm_b == 0 ) {
			return 0;
		}

		return $dot_product / ( sqrt( $norm_a ) * sqrt( $norm_b ) );
	}
}
