<?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\Interfaces\Utility_Interface;
use Limb_Chatbot\Includes\Services\Collection;
use Limb_Chatbot\Includes\Services\Helper;
use Limb_Chatbot\Includes\Services\Widget_Collection;
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[]
	 * @json_excluded
	 */
	const NONE_SETTING_PROPERTIES = [ 'messages' ];

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

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

	/**
	 * The chatbot type (e.g., simple, assistant).
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $type = 'simple';

	/**
	 * Background shape of the chatbot icon.
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $icon_bg_shape = 'squircle';

	public ?array $action_ids = [];

	/**
	 * Whether live agent is enabled.
	 *
	 * @since 1.0.0
	 * @var bool|null
	 * @json_excluded
	 */
	public ?bool $live_agent = false;

	/**
	 * Live agent configuration ID.
	 *
	 * @since 1.0.0
	 * @var int|null
	 * @json_excluded
	 */
	public ?int $live_agent_config_id = null;


	/**
	 * Whether short responses are enabled for live agent.
	 *
	 * @since 1.0.0
	 * @var int|null
	 * @json_excluded
	 */
	public ?int $short_responses = 0;

	/**
	 * Avatar visibility setting for chatbot replies.
	 *
	 * @since 1.0.0
	 * @var null|bool
	 */
	public ?bool $avatar_visibility = false;

	/**
	 * Message fetch method for live agent (Slack/Telegram).
	 * Options: 'polling' or 'event_subscription'
	 *
	 * @since 1.0.12
	 * @var string|null
	 * @json_excluded
	 */
	public ?string $agent_fetch_method = 'polling';

	/**
	 * Array of Chatbot_User IDs configured as live agents.
	 *
	 * @since 1.0.0
	 * @var array
	 * @json_excluded
	 */
	public array $agent_ids = [];


	/**
	 * Size of the chatbot UI (e.g., md, lg).
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $size = 'md';

	/**
	 * Icon ID used for chatbot avatar.
	 *
	 * @since 1.0.0
	 * @var int|null
	 */
	public ?int $icon = 6;


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

	/**
	 * Avatar ID used for chatbot display.
	 *
	 * @since 1.0.0
	 * @var int|null
	 */
	public ?int $avatar = 11; // Made the second icon as default

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

	/**
	 * Configuration ID for this chatbot.
	 *
	 * @since 1.0.0
	 * @var int|null
	 * @json_excluded
	 */
	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;

	/**
	 * External identifier for assistant integration.
	 *
	 * @since 1.0.0
	 * @var string|null
	 * @json_excluded
	 */
	public ?string $assistant_external_id = null;

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

	/**
	 * Whether to enable knowledge base support.
	 *
	 * @since 1.0.0
	 * @var bool|null
	 * @json_excluded
	 */
	public ?bool $knowledge_base = 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;

	/**
	 * Maximum number of similar items to fetch from the vector index.
	 *
	 * @since 1.0.0
	 * @var int|null
	 * @json_excluded
	 */
	public ?int $kb_vector_similarity_count = 3;

	/**
	 * List of tool calls linked to the chatbot.
	 *
	 * @since 1.0.0
	 * @var array|null
	 * @json_excluded
	 */
	public ?array $tools_ids = [];

	/**
	 * Display title of the chatbot widget.
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $title = 'Limb';

	/**
	 * Greeting text shown in the welcome view.
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $greeting_text = 'Hello 👋';

	/**
	 * Tagline shown below the greeting message.
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $greeting_tagline = 'How can we help?';

	/**
	 * Placeholder text for the message input box.
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $message_placeholder = 'Write a message...';

	/**
	 * Custom text for the send button.
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $send_button_text = '';
	/**
	 * Whether the chatbot supports copy message functionality.
	 *
	 * @since 1.0.0
	 * @var bool|null
	 */
	public ?bool $copy = true;

	/**
	 * Whether the chatbot should show reset action in messages (admin Reload toggle).
	 *
	 * @since 1.0.0
	 * @var bool|null
	 */
	public ?bool $reload = true;

	/**
	 * Whether the chatbot should play a sound on agent reply.
	 *
	 * @since 1.0.9
	 * @var bool|null
	 */
	public ?bool $sound = true;

	/**
	 * Whether to show unseen messages badge.
	 *
	 * @since 1.0.0
	 * @var bool|null
	 */
	public ?bool $show_unseen_messages_badge = true;

	/**
	 * Whether to show unseen messages count in the badge.
	 *
	 * @since 1.0.0
	 * @var bool|null
	 */
	public ?bool $show_unseen_messages_count = true;

	/**
	 * Whether the chatbot should show sources action in messages (admin Show knowldege sources toggle).
	 *
	 * @since 1.0.4
	 * @var bool|null
	 */
	public ?bool $show_sources = true;

	/**
	 * Whether the close button is enabled on the chatbot.
	 *
	 * @since 1.0.0
	 * @var bool|null
	 */
	public ?bool $close_button = true;

	/**
	 * Whether voice input is enabled.
	 *
	 * @since 1.0.0
	 * @var bool|null
	 */
	public ?bool $voice = false;
	/**
	 * Whether image messages are enabled.
	 *
	 * @since 1.0.0
	 * @var bool|null
	 */
	public ?bool $image = false;

	/**
	 * Whether to enable cleaning (e.g., clearing state).
	 *
	 * @since 1.0.0
	 * @var bool|null
	 */
	public ?bool $clean = true;

	/**
	 * Theme preference (e.g., light, dark, system).
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $theme = 'system';

	/**
	 * Primary theme color for the chatbot UI.
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $color = '#458cf5';
	/**
	 * Whether this chatbot is a duplicate of another.
	 *
	 * @since 1.0.0
	 * @var bool|null
	 */
	public ?bool $duplicate = false;


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

	/**
	 * Side of the screen where the chatbot is shown.
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $show_in_side = 'right';

	/**
	 * Whether to open the chatbot in fullscreen.
	 *
	 * @since 1.0.0
	 * @var bool|null
	 */
	public ?bool $fullscreen = false;

	/**
	 * Delay in seconds before the chatbot appears.
	 *
	 * @since 1.0.0
	 * @var int|null
	 */
	public ?int $appear_after = 0;
	/**
	 * Storage scheme for attachments (e.g., db, media, aws).
	 *
	 * @since 1.0.0
	 * @var string|null
	 */
	public ?string $attachment_storage_scheme = null;

	/**
	 * Whether image attachments are supported.
	 *
	 * @since 1.0.0
	 * @var bool|null
	 */
	public ?bool $image_attachment_support = false;

	/**
	 * Widget collection associated with the chatbot.
	 *
	 * @since 1.0.0
	 * @var Widget_Collection|null
	 */
	public ?Widget_Collection $widgets = null;

	/**
	 * 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, ! empty( $chatbot->id ), $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|null $is_cpt        Whether this is based on a Custom Post Type.
	 * @param bool     $fetch_lazy     Whether to include properties marked as @lazy.
	 *
	 * @return void
	 */
	public function populate_properties( $setting_prefix, $is_cpt = null, 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 ) && ! ( $property->getType()->getName() == Widget_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;
			}
			// From Cpt
			if ( $is_cpt ) {
				if ( isset( $this->chatbot->{$property_name} ) ) {
					$value = $this->chatbot->{$property_name};
				}
			}
			// 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;
	}

	/**
	 * Gets the external assistant ID.
	 *
	 * @since 1.0.0
	 *
	 * @return string|null External ID or null.
	 */
	public function get_assistant_external_id(): ?string {
		return $this->assistant_external_id;
	}

	/**
	 * 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();
	}

	/**
	 * Gets the chatbot type (e.g. simple, advanced).
	 *
	 * @since 1.0.0
	 *
	 * @return string|null Type value or null.
	 */
	public function get_type(): ?string {
		return $this->type;
	}

	/**
	 * 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;
	}


	/**
	 * Returns whether the knowledge base is enabled.
	 *
	 * @since 1.0.0
	 *
	 * @return bool|null True if enabled, false otherwise.
	 */
	public function get_knowledge_base(): ?bool {
		return $this->knowledge_base;
	}

	/**
	 * 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;
	}

	/**
	 * Gets the knowledge base vector similarity score.
	 *
	 * @since 1.0.0
	 *
	 * @return int|null Similarity score.
	 */
	public function get_kb_vector_similarity_score(): ?int {
		return $this->kb_vector_similarity_score;
	}

	/**
	 * Gets the knowledge base vector similarity count.
	 *
	 * @since 1.0.0
	 *
	 * @return int|null Maximum number of items.
	 */
	public function get_kb_vector_similarity_count(): ?int {
		return $this->kb_vector_similarity_count;
	}

	/**
	 * Returns the current attachment storage scheme.
	 *
	 * @since 1.0.0
	 *
	 * @return string|null Storage scheme name (e.g., db, media, aws).
	 */
	public function get_attachment_storage_scheme(): ?string {
		return $this->attachment_storage_scheme;
	}

	/**
	 * Checks if the chatbot supports image attachments.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if supported, false otherwise.
	 */
	public function is_supporting_image(): bool {
		return ! empty( $this->image_attachment_support );
	}

	/**
	 * Checks whether the current storage scheme is media.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if storage scheme is media, false otherwise.
	 */
	public function is_media_attachment_storage_scheme(): bool {
		return $this->get_attachment_storage_scheme() === 'media';
	}

	/**
	 * Checks whether the current storage scheme is db.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if storage scheme is db, false otherwise.
	 */
	public function is_db_attachment_storage_scheme(): bool {
		return $this->get_attachment_storage_scheme() === 'db';
	}

	/**
	 * Checks whether the current storage scheme is aws.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if storage scheme is aws, false otherwise.
	 */
	public function is_aws_attachment_storage_scheme(): bool {
		return $this->get_attachment_storage_scheme() === 'aws';
	}

	/**
	 * Checks whether the current storage scheme is google_tmp_storage.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if storage scheme is google_tmp_storage, false otherwise.
	 */
	public function is_google_tmp_storage_attachment_storage_scheme(): bool {
		return $this->get_attachment_storage_scheme() === 'google_tmp_storage';
	}

	/**
	 * Returns the default attachment storage scheme.
	 *
	 * @since 1.0.0
	 *
	 * @return string Default scheme (usually 'db').
	 */
	public function get_default_attachment_storage_scheme(): string {
		return 'db';
	}

	/**
	 * 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 ) {
			$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() {
		if ( empty( $this->action_ids ) ) {
			return new Collection();
		}

		return Action::where( [ 'id' => $this->action_ids, 'status' => Action::STATUS_ACTIVE ] );
	}

	public function is_live_agent_tool_call( $message ) {
		$ai_provider_utility = $this->get_ai_provider()->get_ai_providers_utility( $this );

		return $ai_provider_utility->is_live_agent_tool_call( $message );
	}
}
