<?php

namespace Limb_Chatbot\Includes\AI_Providers\Open_Ai\Utilities;

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

/**
 * Class Chatbot_Utility
 *
 * Provides contextual utility functions for chatbot-level operations and configuration handling.
 *
 * @package Limb_Chatbot\Includes\AI_Providers\Open_Ai\Utilities
 * @since 1.0.0
 */
class Chatbot_Utility implements \JsonSerializable {

	use Json_Serializable_Trait;


	/**
	 * The API endpoint to be used for OpenAI communication.
	 *
	 * @var string|null
	 * @since 1.0.0
	 */
	public ?string $endpoint;

	/**
	 * The ID of the OpenAI configuration.
	 *
	 * @var int|null
	 * @since 1.0.0
	 */
	public ?int $config_id = null;

	/**
	 * The ID of the AI model to use.
	 *
	 * @var int|null
	 * @since 1.0.0
	 */
	public ?int $ai_model_id = null;

	/**
	 * Timeout in seconds for API calls. Default is 60.
	 *
	 * @var int|null
	 * @since 1.0.0
	 */
	public ?int $timeout = 60;

	/**
	 * Global utility instance shared across OpenAI services.
	 *
	 * @var Global_Utility
	 * @since 1.0.0
	 */
	public Global_Utility $global_utility;

	/**
	 * List of properties excluded from JSON serialization.
	 *
	 * @var array
	 * @since 1.0.0
	 */
	protected array $excluded_json_properties
		= [
			'global_utility',
			'config_id',
			'ai_model_id',
			'endpoint',
		];

	/**
	 * Chatbot_Utility constructor.
	 *
	 * @param Global_Utility $global_utility Global utility object.
	 * @since 1.0.0
	 */
	public function __construct( $global_utility ) {
		$this->global_utility = $global_utility;
	}


	/**
	 * Populates properties from settings dynamically using reflection.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function populate_properties() {
		$setting_prefix = $this->get_settings_prefix();
		$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() ) {
				// If not built in property type, pass it.
				continue;
			}
			$from_setting = Setting::find( $setting_prefix . $property->getName() )->get_value();
			if ( isset( $from_setting ) ) {
				$this->{$property->getName()} = $from_setting;
			}
		}
	}


	/**
	 * Returns the AI model object by its ID.
	 *
	 * @return AI_Model|null
	 * @since 1.0.0
	 */
	public function get_ai_model() {
		return $this->ai_model_id ? AI_Model::find( $this->ai_model_id ) : null;
	}

	/**
	 * Triggers the endpoint to generate a response.
	 *
	 * @return Message|Tool_Calls_Message
	 * @throws \Limb_Chatbot\Includes\Exceptions\Exception
	 * @since 1.0.0
	 */
	public function generate() {
		return $this->get_endpoint_instance()->generate();
	}

	/**
	 * Handles function tool calls within a tool_calls message.
	 *
	 * @param Tool_Calls_Message $tool_calls_message
	 * @return array
	 * @throws Exception
	 * @since 1.0.0
	 */
	public function tool_call( Tool_Calls_Message $tool_calls_message ) {
		$tool_call_responses = [];
		foreach ( $tool_calls_message->get_tool_calls() as $tool_call ) {
			if ( isset( $tool_call->type ) && $tool_call->type === 'function' ) {
				$tool_call_responses[] = $this->function_tool_call( $tool_call, $tool_calls_message );
			}
		}

		return $tool_call_responses;
	}

	/**
	 * Processes a single function tool call.
	 *
	 * @param object $tool_call
	 * @return Tool_Result_Message|Agent_Operation_Results_Message|null
	 * @throws Exception
	 * @since 1.0.0
	 */
	public function function_tool_call( $tool_call, $tool_calls_message ) {
		if ( empty( $tool_call->function->name ) || empty( $tool_call->function->arguments ) ) {
			throw new Exception( __( '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; // Not sure here
		}
		$arguments = json_decode( $tool_call->function->arguments, true );
		if ( json_last_error() !== JSON_ERROR_NONE ) {
			throw new Exception( __( '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::make( [
			'role'         => 'tool',
			'content'      => $result,
			'tool_call_id' => $tool_call->id,
		] );
	}

	/**
	 * Returns an instance of the Chat_Completion_Endpoint.
	 *
	 * @return Chat_Completion_Endpoint
	 * @since 1.0.0
	 */
	public function get_endpoint_instance() {
		return new Chat_Completion_Endpoint( $this );
	}

	/**
	 * Retrieves the OpenAI config object.
	 *
	 * @return Config|null
	 * @since 1.0.0
	 */
	public function get_config() {
		return $this->config_id ? Config::find( $this->config_id ) : null;
	}

	/**
	 * Returns tools associated with the current chatbot context.
	 *
	 * @return array
	 * @since 1.0.0
	 */
	public function get_tools() {
		$tools = $this->global_utility->get_tools();
		if ( empty( $tools ) ) {
			return $tools;
		}
		$formatted = [];
		foreach ( $tools as $tool ) {
			$formatted[] = [
				'type'            => $tool->get_type(),
				$tool->get_type() => [
					'name'        => $tool->get_name(),
					'description' => $tool->get_description(),
					'parameters'  => Helper::maybe_json_decode( $tool->get_parameters_schema() ),
				],
			];
		}

		return $formatted;
	}


	/**
	 * Returns the timeout value for requests.
	 *
	 * @return int|null
	 * @since 1.0.0
	 */
	public function get_timeout(): ?int {
		return $this->timeout;
	}

	/**
	 * Returns the settings prefix used to locate related values.
	 *
	 * @return string
	 * @since 1.0.0
	 */
	public function get_settings_prefix() {
		return 'ai_providers.' . Open_Ai::$id . '.' . Chatbot::SETTING_PREFIX;
	}
}
