<?php
/**
 * Chatbot Utility for DeepSeek AI Provider.
 *
 * Standalone utility class for DeepSeek chatbot operations.
 * DeepSeek API is OpenAI-compatible but this class is independent.
 *
 * @package Limb_Chatbot\Includes\AI_Providers\Deep_Seek\Utilities
 * @since   1.0.0
 */

namespace Limb_Chatbot\Includes\AI_Providers\Deep_Seek\Utilities;

use Limb_Chatbot\Includes\AI_Providers\Deep_Seek\Deep_Seek;
use Limb_Chatbot\Includes\AI_Providers\Deep_Seek\Endpoints\Chat_Completion_Endpoint;
use Limb_Chatbot\Includes\AI_Providers\Deep_Seek\Endpoints\Chat_Completion\Tool_Calls_Message;
use Limb_Chatbot\Includes\AI_Providers\Deep_Seek\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\Chatbot;
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
 *
 * Standalone utility class for DeepSeek AI provider chatbot operations.
 * Similar structure to Gemini's Chatbot_Utility - does NOT extend OpenAI.
 *
 * @package Limb_Chatbot\Includes\AI_Providers\Deep_Seek\Utilities
 * @since   1.0.0
 */
class Chatbot_Utility {
	use Json_Serializable_Trait;

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

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

	/**
	 * Config ID for API credentials.
	 *
	 * @var int|null
	 * @since 1.0.0
	 */
	public ?int $config_id = null;

	/**
	 * Request timeout in seconds.
	 *
	 * @var int
	 * @since 1.0.0
	 */
	public int $timeout = 60;

	/**
	 * Default timeout for requests in seconds.
	 *
	 * @var int
	 * @since 1.0.0
	 */
	public int $default_timeout = 60;

	/**
	 * Constructor.
	 *
	 * @param Global_Utility $global_utility The global utility instance.
	 *
	 * @since 1.0.0
	 */
	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.0
	 */
	public function populate_properties( bool $is_cpt ): void {
		$setting_prefix = $this->get_settings_prefix();
		$reflection = new ReflectionClass( $this );
		$properties = $reflection->getProperties();

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

			if ( empty( $this->{$property_name} ) && ! $property->isStatic() ) {
				$key = $setting_prefix . $property_name;

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

				$this->{$property_name} = $value;
			}
		}
	}

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

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

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


	/**
	 * Process tool calls by invoking the respective functions.
	 *
	 * DeepSeek uses the same tool call format as OpenAI:
	 * - tool_calls array with function name and arguments
	 * - Returns array of Tool_Result_Message objects
	 *
	 * @param Tool_Calls_Message $tool_calls_message The tool calls message.
	 *
	 * @return array Array of Tool_Result_Message objects.
	 * @throws Exception If tool call processing fails.
	 * @since 1.0.0
	 */
	public function tool_call( Tool_Calls_Message $tool_calls_message ): array {
		$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.
	 *
	 * DeepSeek API uses the same tool call format as OpenAI:
	 * - role: "tool"
	 * - tool_call_id: the ID of the tool call
	 * - content: the result of the tool execution
	 *
	 * @param object             $tool_call          The tool call object from the API response.
	 * @param Tool_Calls_Message $tool_calls_message The parent tool calls message.
	 *
	 * @return Tool_Result_Message|Agent_Operation_Results_Message|null The tool result message or null if tool not found.
	 * @throws Exception If tool call parameters are invalid or arguments fail to decode.
	 * @since 1.0.0
	 */
	public function function_tool_call( $tool_call, Tool_Calls_Message $tool_calls_message ): ?Tool_Result_Message {
		if ( empty( $tool_call->function->name ) || empty( $tool_call->function->arguments ) ) {
			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 = json_decode( $tool_call->function->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::make( [
			'role'         => 'tool',
			'content'      => $result,
			'tool_call_id' => $tool_call->id,
		] );
	}

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

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

	/**
	 * Get settings prefix string for configuration keys.
	 *
	 * @return string
	 * @since 1.0.0
	 */
	public function get_settings_prefix(): string {
		return 'ai_providers.' . Deep_Seek::$id . '.' . Chatbot::SETTING_PREFIX;
	}

	/**
	 * Get the tools in the format required for DeepSeek API.
	 *
	 * DeepSeek uses the same tool format as OpenAI.
	 *
	 * @return array|null
	 * @since 1.0.0
	 */
	public function get_tools(): ?array {
		$chatbot_tools = $this->global_utility->get_tools();

		if ( empty( $chatbot_tools ) ) {
			return null;
		}

		$formatted = [];
		foreach ( $chatbot_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;
	}
}