<?php

namespace Limb_Chatbot\Includes\Services;

use Limb_Chatbot\Includes\Data_Objects\Dataset_Entry;
use Limb_Chatbot\Includes\Data_Objects\Message;
use Limb_Chatbot\Includes\Data_Objects\Vector;
use Limb_Chatbot\Includes\Exceptions\Exception;
use Limb_Chatbot\Includes\Utilities\Chatbot_Utility;
use Limb_Chatbot\Includes\Services\Knowledge\Knowledge_Reranker_Service;

/**
 * Provides contextual enhancements to user messages using vector similarity.
 *
 * Searches related context based on vector embeddings and appends it to the user's message.
 *
 * @since 1.0.0
 */
class Context_Provider_Service {

	/**
	 * Usage service for tracking or validating usage limits during context generation.
	 *
	 * @var Usage_Service|null
	 * @since 1.0.0
	 */
	public ?Usage_Service $usage_service = null;

	/**
	 * Chatbot utility instance containing chatbot and message context.
	 *
	 * @var Chatbot_Utility|null
	 * @since 1.0.0
	 */
	protected ?Chatbot_Utility $chatbot_utility = null;

	/**
	 * Constructor for Context_Provider_Service.
	 *
	 * @param  Chatbot_Utility  $chatbot_utility  The chatbot utility instance.
	 *
	 * @since 1.0.0
	 *
	 */
	public function __construct( Chatbot_Utility $chatbot_utility ) {
		$this->chatbot_utility = $chatbot_utility;
		$this->usage_service   = new Usage_Service( $chatbot_utility->get_limits() );
	}

	/**
	 * Appends context to a user message based on similar vector embeddings.
	 *
	 * @param  Message  $message  The user message.
	 * @param  bool  $in_chat  Whether to respect chat-based vector search eligibility.
	 *
	 * @return array Contextual entries and search query.
	 * @throws Exception
	 * @since 1.0.0
	 */
	public function get_context(
		Message $message,
		bool $in_chat = true,
		$vector_index_ids = [],
		$similarity_score = null
	): array {
		if ( empty( $similarity_score ) ) {
			$similarity_score = $this->chatbot_utility->kb_vector_similarity_score;
		}
		$entries = [];
		if ( empty( $vector_index_ids ) ) {
			$vector_index_ids = $this->chatbot_utility->get_kb_vector_index_ids();
		}
		if ( ! is_array( $vector_index_ids ) ) {
			return $entries;
		}
		if ( $in_chat ) {
			if ( ! $this->is_vector_search_applicable( $message->extract_text() ) ) {
				return $entries;
			}
		}

		foreach ( $vector_index_ids as $vector_index_id ) {
			$vector_searcher = new Vector_Db_Searcher( $this->chatbot_utility, $vector_index_id );
			$similar_vectors = $vector_searcher->search( $message );
			if ( ! is_array( $similar_vectors ) ) {
				continue;
			}
			foreach ( $similar_vectors as $vector ) {
				if ( $this->score_is_passed( $vector, $similarity_score ) ) {
					$entry = $vector->dataset_entry();
					if ( $entry instanceof Dataset_Entry ) {
						$entry->set_score( $vector->score );
						$entries[] = $entry;
					}
				}
			}
		}

		// Apply LLM-based reranking using recent chat messages as context.
		if ( ! empty( $entries ) && $in_chat ) {
			try {
				$reranker              = new Knowledge_Reranker_Service( $this->chatbot_utility );
				$entries               = $reranker->rerank( $entries, $message );
			} catch ( \Throwable $e ) {
				// Fail gracefully and keep heuristic order if reranking fails.
			}
		}

		return $entries;
	}

	/**
	 * Determines whether a given vector’s similarity score passes the defined threshold.
	 *
	 * @param  Vector  $vector  The vector to evaluate.
	 *
	 * @return bool True if the similarity score is above the threshold.
	 * @since 1.0.0
	 *
	 */
	public function score_is_passed( Vector $vector, $similarity_score ): bool {
		if ( isset( $vector->score ) ) {
			$score = (float) $vector->score;
			return round( $score * 100, 2 ) >= $similarity_score;
		}

		return false;
	}

	/**
	 * Builds a message object with a search-optimized query if applicable.
	 *
	 * Determines if the current message is eligible for vector store retrieval.
	 *
	 * @param  string  $user_prompt  Original user message.
	 *
	 * @return bool True if vector search is allowed, false otherwise.
	 * @since 1.0.0
	 */
	private function is_vector_search_applicable( string $user_prompt ) {
		$user_prompt = trim( $user_prompt );

		// 1. Quick bail-outs for empty or trivial input
		if ( $user_prompt === '' ) {
			return false;
		}

		// 2. Count words (multibyte-safe)
		$word_count = count( explode( ' ', ( strip_tags( $user_prompt ) ) ?? [] ) );

		// 3. Skip very short or vague prompts (1–2 words)
		if ( $word_count < 3 ) {
			return false;
		}

		return true;
	}
}