<?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\Query_Intelligence_Service;
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 {
		// Short‑circuit: greeting‑only messages (e.g. "Hello 👋") should not trigger KB search.
		if ( $this->is_greeting_message( $this->user_message->extract_text() ) ) {
			return [];
		}

		$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 );
		$embeddable_message->set_role( Message::ROLE_USER );

		// STEP 1 — Query Intelligence Layer (before any DB or vector access).
		$query_intelligence = new Query_Intelligence_Service( $utility );
		$analysis           = $query_intelligence->analyze( $this->user_message->extract_text() );

		// If nothing meaningful was extracted, skip vector retrieval completely.
		if ( empty( $analysis['normalized'] ) || empty( $analysis['keywords'] ) ) {
			return [];
		}

		// Prefer an LLM-refined query when available, otherwise fall back to normalized text.
		$search_text = $analysis['rewritten'] ?: $analysis['normalized'];

		$embeddable_message->set_content( [
			[
				'type' => 'text',
				'text' => [ 'value' => $search_text ],
			],
		] );

		// Enforce usage limits based on the optimized embeddable message.
		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 )
			);
		}

		// Use the optimized message as the basis for vector search.
		return $context_provider->get_context( $embeddable_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();
	}

	/**
	 * Detects simple greeting messages in a language‑agnostic, regex‑based way.
	 *
	 * Intent:
	 * - Catch short, greeting‑only messages like "Hello", "Hola 👋", "Привет", "Bonjour".
	 * - Avoid blocking normal questions that merely contain a greeting plus real content.
	 *
	 * This is deliberately lightweight and deterministic (no extra LLM calls),
	 * but tuned with multilingual greeting patterns.
	 *
	 * @param  string  $text  Raw user message text.
	 *
	 * @return bool True if the message is classified as a pure greeting.
	 */
	private function is_greeting_message( string $text ): bool {
		$text = trim( $text );
		if ( $text === '' ) {
			return false;
		}

		// Normalize
		$normalized = mb_strtolower( $text );

		// Strip common punctuation at the ends for easier matching.
		$normalized = trim( $normalized, " \t\n\r\0\x0B!?.…，。！？」»«" );

		// Very long or dense messages are unlikely to be just greetings.
		$word_count = preg_match_all( '/\p{L}+/u', $normalized, $matches );
		if ( $word_count > 8 || mb_strlen( $normalized ) > 80 ) {
			return false;
		}

		// If it is mostly a wave / smile emoji with 0‑2 short tokens, treat as greeting.
		if ( preg_match( '/👋/u', $normalized ) && $word_count <= 4 ) {
			return true;
		}

		// Multilingual greeting patterns (case‑insensitive where applicable).
		$patterns = [
			// English
			'/\b(hi|hello|hey|howdy|yo)\b/u',
			'/\b(good (morning|afternoon|evening|day|night))\b/u',
			// Spanish / Portuguese
			'/\b(hola|buenos? (d[ií]as|tardes|noches))\b/u',
			'/\b(ol[áa]|oi|bom dia|boa (tarde|noite))\b/u',
			// French
			'/\b(bonjour|salut|bonsoir)\b/u',
			// German / Dutch / Scandinavian
			'/\b(hallo|guten (morgen|tag|abend)|hej|hei)\b/u',
			// Italian
			'/\b(ciao|buongiorno|buonasera)\b/u',
			// Turkish
			'/\b(merhaba|selam(ün aleyk[üu]m)?|selamün aleyk[üu]m)\b/u',
			// Russian and other Cyrillic greetings
			'/\b(привет|здравствуй|доброе утро|добрый день|добрый вечер)\b/ui',
			// Arabic / Persian / Urdu variants
			'/\b(سلام|السلا?م عليكم|السلام عليكم|salam|salaam|ass?alamu[ -]?alaikum)\b/ui',
			// Hindi and related scripts
			'/\b(नमस्ते|नमस्कार|namaste|namaskar)\b/ui',
			// Armenian (common informal greetings)
			'/\b(բար[եե]վ|բարի (առավոտ|օր|երեկո))\b/ui',
			// Generic fallback: "hi there", "hello there" etc.
			'/\b(hi|hello|hey)[^a-zA-Z0-9]{0,5}(there|again)?\b/u',
		];

		$is_greeting = false;
		foreach ( $patterns as $pattern ) {
			if ( preg_match( $pattern, $normalized ) ) {
				$is_greeting = true;
				break;
			}
		}

		if ( ! $is_greeting ) {
			return false;
		}


		if ( count( explode( ' ', $text ) ) >= 3 ) {
			// If it matches a greeting pattern but has more than 3 words, it's likely not a pure greeting.
			return false;
		}

		/**
		 * Allow external customization of greeting detection.
		 *
		 * @param bool   $is_greeting Current classification.
		 * @param string $text        Original user message text.
		 */
		return (bool) apply_filters( 'lbaic_is_greeting_message', $is_greeting, $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;
	}
}
