<?php

namespace Limb_Chatbot\Includes\Services;

use Limb_Chatbot\Includes\Chatbot_Tools\Actions\Action_Chatbot_Tool;
use Limb_Chatbot\Includes\Chatbot_Tools\Chatbot_Tools;
use Limb_Chatbot\Includes\Data_Objects\Action_Tool_Calls_Message;
use Limb_Chatbot\Includes\Data_Objects\Chat;
use Limb_Chatbot\Includes\Data_Objects\Chatbot;
use Limb_Chatbot\Includes\Data_Objects\Chatbot_Meta;
use Limb_Chatbot\Includes\Data_Objects\Message;
use Limb_Chatbot\Includes\Data_Objects\Tool_Calls_Message;
use Limb_Chatbot\Includes\Data_Schemas;
use Limb_Chatbot\Includes\Database_Strategies\WPDB;
use Limb_Chatbot\Includes\Exceptions\Error_Codes;
use Limb_Chatbot\Includes\Exceptions\Exception;
use Limb_Chatbot\Includes\Repositories\Chatbot_Meta_Repository;
use Limb_Chatbot\Includes\Repositories\Chatbot_Repository;
use Limb_Chatbot\Includes\Services\Actions\Action_Service;
use Limb_Chatbot\Includes\Services\Knowledge\Knowledge_Source_Store;
use Limb_Chatbot\Includes\Services\Knowledge\Knowledge_Source_Tracker;
use Limb_Chatbot\Includes\Services\System_Message\Chatbot_System_Message_Generator;
use Limb_Chatbot\Includes\Utilities\Chatbot_Utility;

/**
 * Service class for managing chatbot operations such as generating replies,
 * message collection, saving, and updating chatbot instances.
 *
 * @since 1.0.0
 */
class Chatbot_Service {

	/**
	 * Repository for chatbot data access.
	 *
	 * @var Chatbot_Repository
	 * @since 1.0.0
	 */
	public Chatbot_Repository $repository;

	/**
	 * Repository for chatbot meta data.
	 *
	 * @var Chatbot_Meta_Repository
	 * @since 1.0.0
	 */
	public Chatbot_Meta_Repository $meta_repository;

	/**
	 * Utility instance for chatbot-related helper methods.
	 *
	 * @var Chatbot_Utility
	 * @since 1.0.0
	 */
	public Chatbot_Utility $utility;

	/**
	 * Data schema instance for chatbot meta validation.
	 *
	 * @var Data_Schemas\Chatbot_Meta
	 * @since 1.0.0
	 */
	public Data_Schemas\Chatbot_Meta $chatbot_meta_schema;
	private Action_Service $action_service;
	private Chat_Language_Service $chat_language_service;


	/**
	 * Constructor.
	 *
	 * @param  Chatbot_Utility|null  $chatbot_utility  Optional chatbot utility instance.
	 *
	 * @since 1.0.0
	 *
	 */
	public function __construct( ?Chatbot_Utility $chatbot_utility = null ) {
		$this->repository          = new Chatbot_Repository();
		$this->meta_repository     = new Chatbot_Meta_Repository();
		$this->chatbot_meta_schema = new Data_Schemas\Chatbot_Meta();
		if ( ! empty( $chatbot_utility ) ) {
			$this->utility = $chatbot_utility;
		}
	}

	public Message_Service $message_service;

	/**
	 * Generates a reply message for a given chat instance.
	 *
	 * @param  Chat  $chat  Chat instance for which to generate reply.
	 *
	 * @return Message Generated reply message.
	 * @throws \Exception On transaction failure or other errors.
	 * @since 1.0.0
	 *
	 */
	public function generate_reply( Chat $chat ): Message {
		$wpdb = new WPDB();
		$wpdb->start_transaction();
		try {
			$this->utility->set_chat( $chat );
			$this->register_chat_context($chat);
			$messages     = $this->get_messages( $chat );
			$last_message = end( $messages );
			// Check for ongoing action (parameter collection)
			$this->action_service = new Action_Service( $chat, $this->utility );
			if ( $ongoing_action_message = $this->action_service->check_ongoing_action( $last_message ) ) {
				$ongoing_action_message->set_chat_uuid( $chat->uuid );
				$ongoing_action_message->save();
				$wpdb->commit_transaction();

				return $ongoing_action_message;
			}
			$user_message = $this->prepare_user_message( $last_message, $messages, $chat );
			$this->collect_messages( $messages, $user_message, $chat );
			// Handle tool calls if any
			$message = $this->maybe_process_tool_calls( $this->utility->generate(), $this->utility, $user_message );
			// Check if this is an action parameter request (skip normal processing)
			if ( $message instanceof Message && $message->is_action_message() || $message->is_live_agent_connection() ) {
				$message->set_chat_uuid( $chat->uuid );
				$message->save();
				$wpdb->commit_transaction();

				return $message;
			}
			$this->update_usage( $message );
			if ( ! $message instanceof Message ) {
				throw new Exception( Error_Codes::AI_RESPONSE_FAILED, __( 'Failed to get AI Response', 'limb-chatbot' ) );
			}
			// After tool calls, the final message save, will be ideal
			$message->set_chat_uuid( $chat->uuid );
			$message->save();
			$this->store_knowledge_sources_from_store( $message );
			$wpdb->commit_transaction();

			return $message->with( 'sources' );
		} catch ( \Exception $e ) {
			$wpdb->rollback_transaction();
			throw $e;
		}
	}

	/**
	 * Collects and prepares messages, including injecting the system message with source instructions and knowledge.
	 *
	 * @param  array  $messages  Messages array to collect and prepare.
	 * @param  Message  $user_message  The user message to fetch knowledge for.
	 * @param  Chat  $chat  The chat object.
	 *
	 * @return void
	 * @since 1.0.0
	 *
	 */
	public function collect_messages( array &$messages, Message $user_message, Chat $chat ) {
		// Fetch knowledge
		$user_prompt_manager = new User_Prompt_Manager( $user_message, $chat );
		$knowledge           = $user_prompt_manager->fetch_knowledge( $this->utility );

		// Store initial sources and context in the singleton store
		$source_store = Knowledge_Source_Store::instance();
		$source_store->set_context( $this->utility, $user_message->extract_text() );
		$source_store->add_initial_sources( $knowledge );

		// Conditionally disable knowledge_search tool when knowledge is available
		new Chatbot_Tools( $this->utility );

		$system_message = ( new Chatbot_System_Message_Generator() )->generate( $this->utility, $knowledge );
		if ( ! $system_message ) {
			$system_message = $this->utility->chatbot->get_parameter( 'internal_system_message' );
		}

		array_unshift( $messages, Message::make( [ 'content' => [ [ 'type' => 'text', 'text' => [ 'value' => $system_message ] ] ], 'role'    => 'system' ] ) );
		$this->utility->set_messages( $messages );
	}

	/**
	 * Retrieves chat messages from storage for a given chat.
	 *
	 * @param  Chat  $chat  Chat instance.
	 *
	 * @return array Array of Message objects.
	 * @since 1.0.0
	 *
	 */
	public function get_messages( Chat $chat ): array {
		$messages = Message::where( [ 'chat_uuid' => $chat->uuid ], null, null, 'id', 'DESC' )->get();

		// Reverse to start from the earlier one
		return array_reverse( $messages );
	}

	/**
	 * Processes tool call messages if applicable and generates a final message.
	 *
	 * @param  Message|null  $message  Initial generated message.
	 * @param  Chatbot_Utility  $utility  Utility instance.
	 * @param  Message  $user_message  User message triggering generation.
	 *
	 * @return Message Processed or original message.
	 * @throws Exception On technical errors during tool call processing.
	 * @since 1.0.0
	 */
	public function maybe_process_tool_calls( ?Message $message, Chatbot_Utility $utility, Message $user_message ) {
		if ( empty( $message ) ) {
			$exception = new Exception( Error_Codes::TECHNICAL_ERROR, __( "Output limit reached. To allow longer responses increase limits in " ) );
			$exception->attach_link( admin_url( 'admin.php?page=lbaic-dashboard&menu=chatbot&tab=costs-limits&utility=output' ), __( 'Single message (output)', 'limb-chatbot' ) );
			throw $exception;
		}
		while ( $message instanceof Tool_Calls_Message ) {
			if ( $message instanceof Action_Tool_Calls_Message ) {
				return $this->init_action($message);
			}
			if ( $live_agent_config = $utility->is_live_agent_tool_call( $message ) ) {
				return $this->init_live_agent( $live_agent_config );
			}
			$message->set_chat_uuid( $user_message->get_chat_uuid() );

			$results = $utility->tool_call( $message );

			if ( empty( $results ) ) {
				throw new Exception( Error_Codes::TECHNICAL_ERROR, __( 'Technical error.', 'limb-chatbot' ) );
			}

			// Extend conversation with tool call + results
			$utility->set_messages(
				array_merge(
					$utility->get_messages(),
					[ $user_message, $message ],
					$results
				)
			);

			// Generate next model response (could be another tool call or final text)
			$message = $utility->generate();
		}

		// Once model returns a non-tool message, we’re done
		return $message;
	}

	/**
	 * Updates usage statistics based on the generated message usage data.
	 *
	 * @param  Message  $message  Message containing usage data.
	 *
	 * @return void
	 * @since 1.0.0
	 *
	 */
	protected function update_usage( Message $message ): void {
		if ( ( $usage = $message->get_usage() ) && $this->utility->get_limits() instanceof Collection ) {
			( new Usage_Service( $this->utility->get_limits() ) )->update_usage( $usage, $this->utility->get_ai_model(), $this->utility->get_chat() );
		}
	}

	/**
	 * Creates a new chatbot instance with optional meta data.
	 *
	 * @param  array  $data  Chatbot creation data including optional metas.
	 *
	 * @return Chatbot|null Created Chatbot instance or null on failure.
	 * @since 1.0.0
	 *
	 */
	public function create( array $data ): ?Chatbot {
		$chatbot = $this->repository->create( $data );
		$metas[] = Chatbot_Meta::create( [ 'post_id' => $chatbot->id, 'meta_key' => '_uuid', 'meta_value' => Helper::get_uuid() ] );
		if ( ! empty( $data['metas'] ) ) {
			$chatbot->included['metas'] = array_merge( $metas, $this->meta_repository->batch_create( $data['metas'], false, $chatbot ) );
		}

		return $chatbot ?? null;
	}

	/**
	 * Updates an existing chatbot instance and its metas.
	 *
	 * @param  int  $chatbot_id  Chatbot ID.
	 * @param  array  $data  Updated data including metas.
	 *
	 * @return Chatbot|null Updated Chatbot instance or null on failure.
	 * @throws \Exception
	 * @since 1.0.0
	 */
	public function update( int $chatbot_id, array $data ): ?Chatbot {
		$chatbot = $this->repository->update( $chatbot_id, $data );
		if ( ! empty( $data['metas'] ) ) {
			$chatbot->included['metas'] = $this->meta_repository->batch_update( $data['metas'], false, $chatbot );
		}

		return $chatbot ?? null;
	}

	/**
	 * Prepare the latest user message for sending to the LLM by injecting conversation state and knowledge context.
	 *
	 * This method:
	 *   1) Validates that the last message is a user message.
	 *   2) Loads and analyzes the current conversation state for the chat.
	 *   3) Builds an enriched user message including knowledge, state, and context.
	 *   4) Replaces the raw user message in the messages array with the enriched version.
	 *
	 * @param Message[] $messages Reference to the array of Message objects.
	 * @param Chat      $chat     The current chat object.
	 * @param Message      $last_message     The last message sent by user
	 *
	 * @return Message Enriched user message ready for LLM consumption.
	 * @throws Exception If there is no active user message.
	 * @since 1.0.0
	 */
	private function prepare_user_message( $last_message, array &$messages, Chat $chat ): Message {
		if ( empty( $messages ) || empty( $last_message ) || $last_message->get_role() !== Message::ROLE_USER ) {
			throw new Exception( Error_Codes::CHATBOT_NO_ACTIVE_USER_MESSAGE, __( 'There is no active user message.', 'limb-chatbot' ) );
		}

		// 1) Ensure conversation state exists and is up-to-date
		$state_manager = new Conversation_State_Manager();
		$state = $state_manager->load_for_chat( $chat->get_uuid() );

		// 2) Build enriched user message with conversation state
		$user_prompt_manager = new User_Prompt_Manager( $last_message, $chat );
		$user_message        = $user_prompt_manager->build( $this->utility, $state );

		// Replace the raw last message with the enriched version
		array_pop( $messages );
		$messages[] = $user_message;

		return $user_message;
	}

	/**
	 * Store knowledge sources from the singleton store to the message metadata.
	 *
	 * @param  Message  $message  The AI response message.
	 *
	 * @return void
	 * @throws \Exception
	 * @since 1.0.0
	 */
	private function store_knowledge_sources_from_store( Message $message ): void {
		$source_store = Knowledge_Source_Store::instance();

		if ( $source_store->has_sources() ) {
			$sources = $source_store->consume_sources();

			$source_tracker = new Knowledge_Source_Tracker();
			$source_tracker->store_sources( $message, $sources );
		}
	}

	private function init_action( Action_Tool_Calls_Message $action_tool_calls_message ) {
		$action_name    = $action_tool_calls_message->get_action_name();
		$action_tool    = Chatbot_Tools::instance()->get_tool( $action_name );

		if ( $action_tool instanceof Action_Chatbot_Tool ) {
			return $this->action_service->dispatch( $action_tool->action );
		}

		return null;
	}


	private function init_live_agent( $live_agent_config ) {
		$service = new Live_Agent_Service();

		return $service->connect_live_agent( $this->utility->get_chat(), $this->utility, $live_agent_config );
	}

	private function register_chat_context(Chat $chat) {
		Chat_Context::instance()->init($chat);
	}
}