<?php

namespace Limb_Chatbot\Includes\AI_Providers\Grok\Utilities;

use Limb_Chatbot\Includes\AI_Providers\Grok\Endpoints\Chat_Completion\Chat_Completion_Endpoint;
use Limb_Chatbot\Includes\AI_Providers\Grok\Endpoints\Chat_Completion\Tool_Calls_Message;
use Limb_Chatbot\Includes\AI_Providers\Grok\Endpoints\Chat_Completion\Tool_Result_Message;
use Limb_Chatbot\Includes\Chatbot_Tools\Chatbot_Tools;
use Limb_Chatbot\Includes\Data_Objects\Agent_Operation_Results_Message;
use Limb_Chatbot\Includes\Data_Objects\AI_Model;
use Limb_Chatbot\Includes\Data_Objects\Config;
use Limb_Chatbot\Includes\Data_Objects\Message;
use Limb_Chatbot\Includes\Data_Objects\Setting;
use Limb_Chatbot\Includes\Exceptions\Error_Codes;
use Limb_Chatbot\Includes\Exceptions\Exception;
use Limb_Chatbot\Includes\Services\Helper;
use Limb_Chatbot\Includes\Traits\Json_Serializable_Trait;
use Limb_Chatbot\Includes\Utilities\Chatbot_Utility as Global_Utility;
use ReflectionClass;

/**
 * Class Chatbot_Utility
 *
 * Utility class to handle chatbot-related functionalities for xAI Provider.
 *
 * @package Limb_Chatbot\Includes\AI_Providers\Grok\Utilities
 * @since 1.0.12
 */
class Chatbot_Utility {

	use Json_Serializable_Trait;

	/**
	 * Global chatbot utility instance.
	 *
	 * @var Global_Utility
	 * @since 1.0.12
	 */
	public Global_Utility $global_utility;

	/**
	 * AI Model ID used for requests.
	 *
	 * @var int|null
	 * @since 1.0.12
	 */
	public ?int $ai_model_id = null;

	/**
	 * Config ID for the xAI configuration.
	 *
	 * @var int|null
	 * @since 1.0.12
	 */
	public ?int $config_id = null;

	/**
	 * Default timeout for requests in seconds.
	 *
	 * @var int
	 * @since 1.0.12
	 */
	public int $timeout = 120;

	/**
	 * List of properties excluded from JSON serialization.
	 *
	 * @var array
	 * @since 1.0.12
	 */
	protected array $excluded_json_properties = array(
		'global_utility',
		'config_id',
		'ai_model_id',
	);

	/**
	 * Constructor.
	 *
	 * @param  Global_Utility  $global_utility  The global utility instance.
	 *
	 * @since 1.0.12
	 */
	public function __construct( Global_Utility $global_utility ) {
		$this->global_utility = $global_utility;
		$this->populate_properties( ! empty( $this->global_utility->get_chatbot()->get_id() ) );
	}

	/**
	 * Populate object properties either from the CPT meta or Settings.
	 *
	 * @param  bool  $is_cpt  Whether the chatbot is a CPT (custom post type).
	 *
	 * @return void
	 * @since 1.0.12
	 */
	public function populate_properties( $is_cpt ) {
		$reflection = new ReflectionClass( $this );
		$properties = $reflection->getProperties();

		foreach ( $properties as $property ) {
			$property_name = $property->getName();

			if ( in_array( $property_name, $this->excluded_json_properties, true ) ) {
				continue;
			}

			if ( empty( $this->{$property_name} ) && ! $property->isStatic() ) {
				$key = self::class . '_' . $property_name;

				if ( $is_cpt ) {
					$value = $this->global_utility->get_chatbot()->get_meta( $property_name );
				} else {
					$value = Setting::find( $key )->get_value();
				}

				if ( $value !== null ) {
					$this->{$property_name} = $value;
				}
			}
		}
	}

	/**
	 * Generate content by delegating to the endpoint.
	 *
	 * @return Message|Tool_Calls_Message|null Generated message result.
	 * @throws Exception If generation fails.
	 * @since 1.0.12
	 */
	public function generate() {
		return $this->get_endpoint_instance()->generate();
	}

	/**
	 * Finalize and generate response with optional streaming.
	 *
	 * @return Message|Tool_Calls_Message|null Generated message result.
	 * @throws Exception If generation fails.
	 * @since 1.0.12
	 */
	public function finalize() {
		return $this->get_endpoint_instance()->finalize();
	}

	/**
	 * Get instance of the Chat Completion Endpoint.
	 *
	 * @return Chat_Completion_Endpoint
	 * @since 1.0.12
	 */
	public function get_endpoint_instance() {
		return new Chat_Completion_Endpoint( $this );
	}

	/**
	 * Get the AI model instance based on the AI model ID.
	 *
	 * @return AI_Model|null
	 * @since 1.0.12
	 */
	public function get_ai_model() {
		return $this->ai_model_id ? AI_Model::find( $this->ai_model_id ) : null;
	}

	/**
	 * Process a tool call message by invoking the respective functions.
	 *
	 * @param  Tool_Calls_Message  $tool_calls_message  The tool calls message.
	 *
	 * @return array Array of Tool_Result_Message objects.
	 * @throws Exception If the function call is invalid.
	 * @since 1.0.12
	 */
	public function tool_call( Tool_Calls_Message $tool_calls_message ) {
		$tool_call_responses = array();

		foreach ( $tool_calls_message->get_tool_calls() as $tool_call ) {
			if ( isset( $tool_call->type ) && $tool_call->type === 'function' ) {
				$result = $this->function_tool_call( $tool_call, $tool_calls_message );
				if ( $result ) {
					$tool_call_responses[] = $result;
				}
			}
		}

		return $tool_call_responses;
	}

	/**
	 * Processes a single function tool call.
	 *
	 * @param  object  $tool_call  The tool call object.
	 * @param  Tool_Calls_Message  $tool_calls_message  The parent tool calls message.
	 *
	 * @return Tool_Result_Message|Agent_Operation_Results_Message|null The result message or null.
	 * @throws Exception If tool call is invalid.
	 * @since 1.0.12
	 */
	public function function_tool_call( $tool_call, $tool_calls_message ) {
		if ( empty( $tool_call->function->name ) ) {
			throw new Exception(
				Error_Codes::VALIDATION_INVALID_VALUE,
				__( 'Invalid params for tool_calls', 'limb-chatbot' )
			);
		}

		$tool_name = $tool_call->function->name;
		$tool      = Chatbot_Tools::instance()->get_tool( $tool_name );

		if ( ! $tool ) {
			return null;
		}

		$arguments = $tool_call->function->arguments ?? '{}';
		$arguments = json_decode( $arguments, true );

		if ( json_last_error() !== JSON_ERROR_NONE ) {
			throw new Exception(
				Error_Codes::VALIDATION_INVALID_VALUE,
				__( 'Failed to decode tool arguments', 'limb-chatbot' )
			);
		}

		$result = $tool->execute( $arguments, $tool_calls_message, $this->global_utility );
		if ( $result instanceof Agent_Operation_Results_Message ) {
			return $result;
		}

		return Tool_Result_Message::from_tool_call( $tool_call->id, $result );
	}

	/**
	 * Get system instructions from messages.
	 *
	 * @return string|false The system instructions string or false if not available.
	 * @since 1.0.12
	 */
	public function get_system_instructions() {
		$messages = $this->global_utility->get_messages();

		if ( ! empty( $messages[0] ) && $messages[0]->get_role() === 'system' ) {
			$content = $messages[0]->get_content();
			if ( is_array( $content ) && isset( $content[0]['text']['value'] ) ) {
				return $content[0]['text']['value'];
			}
			if ( is_string( $content ) ) {
				return $content;
			}
		}

		return false;
	}

	/**
	 * Get the config instance if config_id is set.
	 *
	 * @return Config|null
	 * @since 1.0.12
	 */
	public function get_config() {
		return ! empty( $this->config_id ) ? Config::find( $this->config_id ) : null;
	}

	/**
	 * Get timeout value in seconds.
	 *
	 * @return int
	 * @since 1.0.12
	 */
	public function get_timeout() {
		return $this->timeout;
	}

	/**
	 * Get tools formatted for xAI API.
	 *
	 * @return array Array of tools in OpenAI-compatible format.
	 * @since 1.0.12
	 */
	public function get_tools() {
		$tools = $this->global_utility->get_tools();

		if ( empty( $tools ) ) {
			return array();
		}

		$formatted = array();
		foreach ( $tools as $tool ) {
			$formatted[] = array(
				'type'     => 'function',
				'function' => array(
					'name'        => $tool->get_name(),
					'description' => $tool->get_description(),
					'parameters'  => Helper::maybe_json_decode( $tool->get_parameters_schema() ),
				),
			);
		}

		return $formatted;
	}

	/**
	 * Get message tools for finalize.
	 *
	 * @return array Array of tools.
	 * @since 1.0.12
	 */
	public function message() {
		return $this->get_tools();
	}
}
