<?php

namespace Limb_Chatbot\Includes\Services;

use Limb_Chatbot\Includes\Data_Objects\Chat;
use Limb_Chatbot\Includes\Data_Objects\Conversation_State;
use Limb_Chatbot\Includes\Data_Objects\Embeddable_Message;
use Limb_Chatbot\Includes\Data_Objects\Message;
use Limb_Chatbot\Includes\Exceptions\Error_Codes;
use Limb_Chatbot\Includes\Exceptions\Exception;
use Limb_Chatbot\Includes\Services\Knowledge\Knowledge_Source_Store;
use Limb_Chatbot\Includes\Utilities\Chatbot_Utility;

/**
 * Class User_Prompt_Manager
 *
 * Responsible for building professional, structured prompts for the AI chatbot.
 * Includes conversation state, knowledge base context, and user preferences.
 *
 * @package Limb_Chatbot\Includes\Services
 * @since 1.0.0
 */
class User_Prompt_Manager {

	/**
	 * User message object.
	 *
	 * @var Message
	 * @since 1.0.0
	 */
	protected Message $user_message;

	/**
	 * Optional chat object associated with the user message.
	 *
	 * @var Chat|null
	 * @since 1.0.0
	 */
	protected ?Chat $chat;

	/**
	 * Array containing all dataset entry sources.
	 *
	 * @var array
	 * @since 1.0.0
	 */
	public array $knowledge = [];

	/**
	 * Maximum number of knowledge entries to include in prompt.
	 * Prevents overly long prompts and reduces hallucinations.
	 *
	 * @var int
	 * @since 1.0.0
	 */
	private int $max_knowledge_entries = 5;

	/**
	 * Maximum total characters for knowledge context.
	 * Helps keep responses concise and focused.
	 *
	 * @var int
	 * @since 1.0.0
	 */
	private int $max_knowledge_chars = 3000;

	/**
	 * Constructor.
	 *
	 * @param  Message  $user_message  User message object.
	 * @param  Chat|null  $chat  Optional chat object.
	 *
	 * @since 1.0.0
	 */
	public function __construct( Message $user_message, ?Chat $chat = null ) {
		$this->user_message = $user_message;
		$this->chat         = $chat;
	}

	/**
	 * Build the final user prompt with clean user message only.
	 * Knowledge is now injected into the system message, not the user message.
	 *
	 * @param  Chatbot_Utility  $utility  Utility instance for accessing KB and services.
	 * @param  Conversation_State|null  $state  Optional conversation state.
	 *
	 * @return Message Clean user message as a Message object.
	 * @since 1.0.0
	 */
	public function build( Chatbot_Utility $utility, ?Conversation_State $state = null ): Message {
		// Compose the clean prompt text (without knowledge)
		$prompt_text = $this->compose_prompt_text( $state );

		$message = new Message();
		$message->set_role( Message::ROLE_USER );
		$message->set_content( [
			[
				'type' => 'text',
				'text' => [ 'value' => $prompt_text ],
			],
		] );
		$message->set_chat_uuid( $this->user_message->get_chat_uuid() );

		return $message;
	}

	/**
	 * Fetch relevant knowledge entries from the knowledge base.
	 *
	 * @param  Chatbot_Utility  $utility  Utility instance providing access to KB.
	 *
	 * @return array Array of knowledge entries relevant to the user message.
	 * @throws Exception When usage limits are exceeded.
	 * @since 1.0.0
	 */
	public function fetch_knowledge( Chatbot_Utility $utility ): array {
		$context_provider   = new Context_Provider_Service( $utility );
		$embeddable_message = new Embeddable_Message( $this->user_message->get_data() );
		$embeddable_message->set_vector_index_ids( $utility->get_kb_vector_index_ids() );
		$embeddable_message->set_chat_uuid( $utility->chat->uuid );

		if ( $limit_exceeded = $context_provider->usage_service->limits_passed( $embeddable_message ) ) {
			throw new Exception(
				Error_Codes::CHATBOT_LIMIT_EXCEED,
				$context_provider->usage_service->get_error_message( $limit_exceeded )
			);
		}

		return $context_provider->get_context( $this->user_message, (bool) $this->chat );
	}

	/**
	 * Compose the clean user prompt text with only the user message.
	 * Knowledge is now injected into the system message, not here.
	 *
	 * @param  Conversation_State|null  $state  Optional conversation state.
	 *
	 * @return string Clean user message text ready to be sent to AI.
	 * @since 1.0.0
	 */
	protected function compose_prompt_text( ?Conversation_State $state = null ): string {
		// Return only the clean user message
		// Knowledge is now injected into the system message
		return $this->user_message->extract_text();
	}

	/**
	 * Format entities in a readable, professional manner.
	 *
	 * @param  array|null  $entities  Entities array.
	 *
	 * @return string Formatted entities string.
	 * @since 1.0.0
	 */
	private function format_entities( ?array $entities ): string {
		if ( empty( $entities ) ) {
			return '(none identified)';
		}

		$formatted = [];
		foreach ( $entities as $category => $items ) {
			if ( ! empty( $items ) ) {
				$formatted[] = ucfirst( $category ) . ': ' . implode( ', ', $items );
			}
		}

		return empty( $formatted ) ? '(none identified)' : implode( '; ', $formatted );
	}

	/**
	 * Format intent history as a readable progression.
	 *
	 * @param  array  $intents  Intent history array.
	 *
	 * @return string Formatted intent progression string.
	 * @since 1.0.0
	 */
	private function format_intent_history( array $intents ): string {
		if ( empty( $intents ) ) {
			return '(none)';
		}

		$formatted = [];
		foreach ( $intents as $index => $intent ) {
			$formatted[] = ( $index + 1 ) . ". " . ucfirst( $intent );
		}

		return implode( ' → ', $formatted );
	}

	/**
	 * Format user preferences into a readable string.
	 *
	 * @param  array  $preferences  User preferences array.
	 *
	 * @return string Formatted preferences string.
	 * @since 1.0.0
	 */
	private function format_user_preferences( array $preferences ): string {
		if ( empty( $preferences ) ) {
			return '(none)';
		}

		$formatted = [];
		foreach ( $preferences as $key => $value ) {
			$formatted[] = ucfirst( $key ) . ': ' . $value;
		}

		return implode( '; ', $formatted );
	}

	/**
	 * Get source information for a dataset entry.
	 *
	 * @param  \Limb_Chatbot\Includes\Data_Objects\Dataset_Entry  $dataset_entry  Dataset entry object.
	 *
	 * @return string|null Source information string or null if not available.
	 * @since 1.0.0
	 */
	private function get_source_info( $dataset_entry ): ?string {
		$dataset = $dataset_entry->dataset();
		if ( ! $dataset ) {
			return null;
		}

		$dataset_name = $dataset->get_name();
		$source_url   = $dataset->source_url();

		$info_parts = [];
		if ( ! empty( $dataset_name ) ) {
			$info_parts[] = "Source: $dataset_name";
		}
		if ( ! empty( $source_url ) ) {
			$info_parts[] = "URL: $source_url";
		}

		return ! empty( $info_parts ) ? implode( ' | ', $info_parts ) : null;
	}
}
