<?php

namespace Limb_Chatbot\Includes\Utilities;

use JsonSerializable;
use Limb_Chatbot\Includes\Chatbot_Tools\Chatbot_Tools;
use Limb_Chatbot\Includes\Data_Objects\Action;
use Limb_Chatbot\Includes\Data_Objects\Chat;
use Limb_Chatbot\Includes\Data_Objects\Chatbot;
use Limb_Chatbot\Includes\Data_Objects\Config;
use Limb_Chatbot\Includes\Data_Objects\Limit;
use Limb_Chatbot\Includes\Data_Objects\AI_Model;
use Limb_Chatbot\Includes\Data_Objects\Output_Limit;
use Limb_Chatbot\Includes\Data_Objects\Tool_Result_Message;
use Limb_Chatbot\Includes\Exceptions\Exception;
use Limb_Chatbot\Includes\Integrations\Integrations;
use Limb_Chatbot\Includes\Interfaces\Utility_Interface;
use Limb_Chatbot\Includes\Services\Collection;
use Limb_Chatbot\Includes\Services\Helper;
use Limb_Chatbot\Includes\Services\User_Manager;
use Limb_Chatbot\Includes\Services\User_Roles;
use Limb_Chatbot\Includes\Traits\Json_Serializable_Trait;
use ReflectionClass;

/**
 * Chatbot_Utility class.
 *
 * Utility handler for chatbot-related configuration and operational logic.
 * Populates chatbot settings, interacts with tools, handles AI model resolution, and more.
 *
 * @package Limb_Chatbot\Includes\Utilities
 * @since 1.0.0
 */
class Chatbot_Utility extends Utility implements Utility_Interface, JsonSerializable {

	use Json_Serializable_Trait;

	/**
	 * Human-readable name for the utility.
	 *
	 * @since 1.0.0
	 * @var string
	 */
	public static string $name = 'Chatbot Utility';

	/**
	 * List of property names that should not be automatically populated from the settings.
	 *
	 * @since 1.0.0
	 * @var string[]
	 */
	const NONE_SETTING_PROPERTIES = [ 'messages', 'chatbot', 'chat', 'integrations_collection', 'name' ];

	/**
	 * The Chatbot instance associated with this utility.
	 *
	 * @since 1.0.0
	 * @var Chatbot
	 */
	public Chatbot $chatbot;

	/**
	 * Array of message data.
	 *
	 * Excluded from automatic population and serialization.
	 *
	 * @since 1.0.0
	 * @var array|null
	 */
	public ?array $messages = [];

	/**
	 * Array of integration identifiers linked to the chatbot.
	 *
	 * @since 1.0.0
	 * @var array|null
	 */
	public ?array $integrations = [];

	/**
	 * Chat instance associated with the current conversation.
	 *
	 * @since 1.0.0
	 * @var Chat|null
	 */
	public ?Chat $chat = null;

	/**
	 * AI provider identifier.
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $ai_provider_id = null;

	/**
	 * Configuration ID for this chatbot.
	 *
	 * @since 1.0.0
	 * @var int|null
	 */
	public ?int $config_id = null;

	/**
	 * Identifier for the AI model.
	 *
	 * @since 1.0.0
	 * @var int|null
	 * @json_excluded
	 */
	public ?int $ai_model_id = null;

	/**
	 * Whether to enable AI streaming responses.
	 *
	 * @since 1.0.0
	 * @var bool|null
	 */
	public ?bool $stream = false;

	/**
	 * Knowledge base vector index identifiers.
	 *
	 * @since 1.0.0
	 * @var array|null
	 * @json_excluded
	 */
	public ?array $kb_vector_index_ids = null;

	/**
	 * Minimum similarity score for KB vector matching.
	 *
	 * @since 1.0.0
	 * @var int|null
	 * @json_excluded
	 */
	public ?int $kb_vector_similarity_score = 30;

	/**
	 * List of chatbot limits such as token caps.
	 *
	 * @since 1.0.0
	 * @var Collection|null
	 */
	public ?Collection $limits = null;

	/**
	 * @var Collection
	 * @lazy
	 */
	private Collection $integrations_collection;

	/**
	 * Chatbot_Utility constructor.
	 *
	 * Initializes the utility with a given Chatbot object and populates its properties.
	 *
	 * @since 1.0.0
	 *
	 * @param Chatbot $chatbot     Chatbot instance used for populating settings.
	 * @param bool    $fetch_lazy  Whether to load lazily annotated properties.
	 */
	public function __construct( Chatbot $chatbot, bool $fetch_lazy = false ) {
		$this->chatbot = $chatbot;
		$this->populate_properties( Chatbot::SETTING_PREFIX, $fetch_lazy );
	}

	/**
	 * Populates class properties using values from the Chatbot object.
	 *
	 * Skips properties marked with @lazy or listed in NONE_SETTING_PROPERTIES.
	 *
	 * @since 1.0.0
	 *
	 * @param string   $setting_prefix Prefix for property names (used for meta lookup).
	 * @param bool     $fetch_lazy     Whether to include properties marked as @lazy.
	 *
	 * @return void
	 */
	public function populate_properties( $setting_prefix, bool $fetch_lazy = false ): void {
		$reflection = new ReflectionClass( $this );
		$properties = $reflection->getProperties();
		foreach ( $properties as $property ) {
			if ( $property->getDeclaringClass()->getName() !== $reflection->getName() ) {
				// If not own property, pass it.
				continue;
			}
			if ( ! $property->getType() || ( ! $property->getType()->isBuiltin() && ! ( $property->getType()->getName() == Collection::class ) ) ) {
				// If not built in property type, pass it.
				continue;
			}
			$property_name = $property->getName();
			if ( in_array( $property_name, self::NONE_SETTING_PROPERTIES ) ) {
				continue;
			}
			if ( ! $fetch_lazy && $property->getDocComment() && str_contains( $property->getDocComment(), '@lazy' ) ) {
				continue;
			}
			// Maybe property was direct property of the CPT based Chatbot(Ex: title, created_at, etc ...), then don't touch
			if ( empty( $value ) ) {
				$value = $this->chatbot->get_parameter( $property_name );
			}
			// Otherwise leave as it is
			if ( isset( $value ) ) {
				$this->{$property_name} = Helper::cast_value( $property->getType()->getName(), $value );
			}
			unset( $value );
		}
	}

	/**
	 * Retrieves the AI provider ID.
	 *
	 * @since 1.0.0
	 *
	 * @return string|null AI provider identifier or null if not set.
	 */
	public function get_ai_provider_id(): ?string {
		return $this->ai_provider_id;
	}

	/**
	 * Executes a tool call using the current AI provider and the provided message.
	 *
	 * @param  mixed  $message  Message or payload to send to the tool.
	 *
	 * @return Tool_Result_Message[] List of result messages from tool response.
	 * @throws Exception
	 * @since 1.0.0
	 *
	 */
	public function tool_call( $message ) {
		$ai_provider_utility = $this->get_ai_provider()->get_ai_providers_utility( $this );
		$result_messages     = $ai_provider_utility->tool_call( $message );

		return $result_messages instanceof Tool_Result_Message ? [ $result_messages ] : ( $result_messages[0] instanceof Tool_Result_Message ? $result_messages : [] );
	}

	/**
	 * Returns the associated Chatbot object.
	 *
	 * @since 1.0.0
	 *
	 * @return Chatbot Chatbot object.
	 */
	public function get_chatbot(): Chatbot {
		return $this->chatbot;
	}

	/**
	 * Retrieves the current message array.
	 *
	 * @since 1.0.0
	 *
	 * @return array|null List of messages or null.
	 */
	public function get_messages(): ?array {
		return $this->messages;
	}

	/**
	 * Sets the message array.
	 *
	 * @since 1.0.0
	 *
	 * @param array|null $messages Message list to assign.
	 *
	 * @return void
	 */
	public function set_messages( ?array $messages ): void {
		$this->messages = $messages;
	}

	/**
	 * Sets the current Chat object.
	 *
	 * @since 1.0.0
	 *
	 * @param Chat|null $chat Chat instance to set.
	 *
	 * @return void
	 */
	public function set_chat( ?Chat $chat ): void {
		$this->chat = $chat;
	}

	/**
	 * Retrieves the current Chat object.
	 *
	 * @since 1.0.0
	 *
	 * @return Chat|null Chat instance or null if not set.
	 */
	public function get_chat(): ?Chat {
		return $this->chat;
	}

	/**
	 * Gets the configuration ID.
	 *
	 * @since 1.0.0
	 *
	 * @return int|null Configuration ID or null.
	 */
	public function get_config_id(): ?int {
		return $this->config_id;
	}

	/**
	 * Retrieves the AI model instance based on the stored ID.
	 *
	 * @since 1.0.0
	 *
	 * @return AI_Model|null AI model or null if not found.
	 */
	public function get_ai_model(): ?AI_Model {
		return AI_Model::find( $this->ai_model_id );
	}

	/**
	 * Retrieves the chatbot's configuration model.
	 *
	 * @since 1.0.0
	 *
	 * @return Config|null Configuration model or null.
	 */
	public function get_config(): ?Config {
		return $this->config_id ? Config::find( $this->config_id ) : null;
	}

	/**
	 * Retrieves all tools linked to this chatbot by ID.
	 *
	 * @since 1.0.0
	 *
	 * @return array|null Collection of Chatbot_Tool instances or null.
	 */
	public function get_tools() {
		$tools = Chatbot_Tools::instance();

		return $tools->get_tools();
	}


	/**
	 * Determines if AI streaming is enabled.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if stream is enabled, false otherwise.
	 */
	public function get_stream(): bool {
		return $this->stream;
	}

	/**
	 * Gets the list of vector index IDs.
	 *
	 * @since 1.0.0
	 *
	 * @return array|null Array of IDs or null.
	 */
	public function get_kb_vector_index_ids(): ?array {
		return $this->kb_vector_index_ids;
	}

	/**
	 * Calculates the maximum token count allowed for AI output based on limits.
	 *
	 * @since 1.0.0
	 *
	 * @return int|null Maximum token count or null if not defined.
	 */
	public function get_max_completion_tokens(): ?int {
		if ( ! $this->get_limits() instanceof Collection ) {
			return null;
		}
		$limit = $this->get_limits()->filter( function ( Limit $item ) {
			return $item instanceof Output_Limit;
		} )->first();
		if ( ! $limit ) {
			return null;
		}
		$model      = $this->get_ai_model();
		$max_tokens = $model->get_output_token_limit();
		if ( $limit->get_unit() === Limit::UNIT_TOKEN ) {
			return min( $limit->get_value(), $max_tokens );
		}
		$output_token_cost = $model->get_meta( 'output_token_cost' );
		if($output_token_cost == 0){
			return null;
		}

		if ( $output_token_cost ) {
			$estimated_tokens = $limit->get_value() / $output_token_cost;

			return min( (int) $estimated_tokens, $max_tokens );
		}

		return $max_tokens;
	}

	/**
	 * Returns the collection of limits for this chatbot.
	 *
	 * @since 1.0.0
	 *
	 * @return Collection|null Limit collection or null.
	 */
	public function get_limits() {
		return $this->limits;
	}

	public function get_actions() {
		$actions = Action::where( [ 'status' => Action::STATUS_ACTIVE, 'type' => Action::TYPE_CONVERSATIONAL ] );
		$current_user_roles = User_Roles::resolve_current_chatbot_user_roles( User_Manager::instance()->get_current_user() );

		return $actions->filter( static function ( $action ) use ( $current_user_roles ) {
			if ( ! $action instanceof Action ) {
				return false;
			}

			$access_roles = $action->get_access_roles();
			if ( ! is_array( $access_roles ) || empty( $access_roles ) ) {
				// Backward compatibility: when no access roles are stored, keep action visible.
				return true;
			}

			$normalized_roles = User_Roles::normalize_roles( $access_roles );

			if ( in_array( Action::ROLE_EVERYONE, $normalized_roles, true ) ) {
				return true;
			}

			return ! empty( array_intersect( $normalized_roles, $current_user_roles ) );
		} );
	}

	public function get_integrations() {
		$collection = new Collection();
		if ( empty( $this->integrations ) ) {
			return $collection;
		}
		if ( ! empty( $this->integrations_collection ) ) {
			return $this->integrations_collection;
		}
		$integrations = Integrations::instance();
		foreach ( $this->integrations as $integration ) {
			if ( $value = $integrations->get_integration( $integration ) ) {
				$collection->push_item( $value );
			}
		}
		$this->integrations_collection = $collection;

		return $collection;
	}
}
