<?php

namespace Limb_Chatbot\Includes\Data_Objects;

use Limb_Chatbot\Includes\Services\Helper;
use Limb_Chatbot\Includes\Services\Knowledge\Knowledge_Source_Tracker;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Users_Controller;

/**
 * Class Message
 *
 * Represents a chat message within the chatbot system.
 *
 * @since 1.0.0
 */
class Message extends WPDB_Data_Object {

	/**
	 * Database table name.
	 *
	 * @since 1.0.0
	 */
	const TABLE_NAME = 'lbaic_chat_messages';

	/**
	 * Role constant for user messages.
	 *
	 * @since 1.0.0
	 */
	const ROLE_USER = 'user';

	/**
	 * Role constant for assistant messages.
	 *
	 * @since 1.0.0
	 */
	const ROLE_ASSISTANT = 'assistant';

	/**
	 * Role constant for system messages.
	 *
	 * @since 1.0.0
	 */
	const ROLE_SYSTEM = 'system';

	/**
	 * Role constant for developer messages.
	 *
	 * @since 1.0.0
	 */
	const ROLE_DEVELOPER = 'developer';

	/**
	 * List of fillable properties for mass assignment.
	 *
	 * @since 1.0.0
	 *
	 * @var string[]
	 */
	const FILLABLE = [ 'uuid', 'chat_uuid', 'user_id', 'role', 'content', 'created_at', 'updated_at' ];
	const CONTENT_TYPE_TEXT = 'text';
	const CONTENT_TYPE_PARAMETER = 'parameter';
	const CONTENT_TYPE_ACTION_SUBMISSION = 'action_submission';
	const CONTENT_TYPE_ACTION_CANCELLATION = 'action_cancellation';
	const CONTENT_TYPE_UNDO_PARAMETER = 'undo_parameter';
	const CONTENT_TYPE_PARAMETER_VALUE = 'parameter_value';
	const DEFAULT_SOURCES_COUNT = 1;

	/**
	 * Primary key ID.
	 *
	 * @var int|null
	 * @json_excluded
	 * @since 1.0.0
	 */
	public ?int $id;

	/**
	 * UUID of the message.
	 *
	 * @var string|null
	 * @since 1.0.0
	 */
	public ?string $uuid;

	/**
	 * UUID of the chat this message belongs to.
	 *
	 * @var string|null
	 * @since 1.0.0
	 */
	public ?string $chat_uuid;

	/**
	 * Role of the message sender.
	 *
	 * @var string|null
	 * @since 1.0.0
	 */
	public ?string $role;

	/**
	 * Message content structured as array.
	 *
	 * @var array|null
	 * @since 1.0.0
	 */
	public ?array $content = null;

	/**
	 * ID of the user who sent the message.
	 *
	 * @json_excluded
	 * @var int|null
	 * @since 1.0.0
	 */
	public ?int $user_id;

	/**
	 * Message creation timestamp.
	 *
	 * @var string|null
	 * @since 1.0.0
	 */
	public ?string $created_at;

	/**
	 * Message update timestamp.
	 *
	 * @var string|null
	 * @since 1.0.0
	 */
	public ?string $updated_at;

	/**
	 * Whether message is embeddable.
	 *
	 * @var bool
	 * @json_excluded
	 * @since 1.0.0
	 */
	public bool $embeddable = false;

	/**
	 * Whether message was streamed.
	 *
	 * @var bool
	 * @since 1.0.0
	 */
	public bool $streamed = false;
	/**
	 * Excluded from JSON serialization.
	 *
	 * @var array
	 * @json_excluded
	 * @since 1.0.0
	 */
	public array $included = [];
	/**
	 * Metadata properties loaded on demand.
	 *
	 * @var array
	 * @json_excluded
	 * @since 1.0.0
	 */
	public array $meta_properties = [
		'source_link',
		'dataset_entries',
		'metas',
		'previous_message',
		'sources',
		'agents',
		'agent',
		'widget_messages'
	];
	/**
	 * @json_excluded
	 * @var bool
	 */
	public bool $is_action_message = false;

	/**
	 * Token usage data associated with this message.
	 *
	 * @var Token_Usage|null
	 * @json_excluded
	 * @since 1.0.0
	 */
	protected ?Token_Usage $usage = null;

	/**
	 * Message constructor.
	 *
	 * @param  array|null|object|string|null  $instance  Initial data to populate the message.
	 *
	 * @since 1.0.0
	 */
	public function __construct( $instance = null ) {
		if ( is_array( $instance ) && ! empty( $instance['content'] ) && $this->isJson( $instance['content'] ) ) {
			$instance['content'] = Helper::maybe_json_decode( $instance['content'] );
		}
		parent::__construct( $instance );
	}

	/**
	 * Delete messages matching conditions.
	 *
	 * @param  array  $where  Conditions for deletion.
	 *
	 * @return bool True on success, false on failure.
	 * @since 1.0.0
	 */
	public static function delete( $where ): bool {
		return static::get_db_strategy()->delete( $where, static::TABLE_NAME );
	}

	/**
	 * Get array of allowed message roles.
	 *
	 * @return string[]
	 * @since 1.0.0
	 */
	public static function get_allowed_roles(): array {
		return [
			static::get_role_system(),
			static::get_role_user(),
			static::get_role_assistant(),
		];
	}

	/**
	 * Get system role string.
	 *
	 * @return string
	 * @since 1.0.0
	 */
	public static function get_role_system(): string {
		return static::ROLE_SYSTEM;
	}

	/**
	 * Get user role string.
	 *
	 * @return string
	 * @since 1.0.0
	 */
	public static function get_role_user(): string {
		return static::ROLE_USER;
	}

	/**
	 * Get assistant role string.
	 *
	 * @return string
	 * @since 1.0.0
	 */
	public static function get_role_assistant(): string {
		return static::ROLE_ASSISTANT;
	}

	/**
	 * Get allowed mime types for attachments.
	 *
	 * @return array<string, string>
	 * @since 1.0.0
	 */
	public static function get_allowed_mime_types(): array {
		return array_intersect( get_allowed_mime_types(), [
			'jpg|jpeg|jpe' => 'image/jpeg',
			'gif'          => 'image/gif',
			'png'          => 'image/png',
			// Add more types as needed
		] );
	}

	/**
	 * Get the UUID of this message.
	 *
	 * @return string|null
	 * @since 1.0.0
	 */
	public function get_uuid(): ?string {
		return $this->uuid;
	}

	/**
	 * Set the UUID of this message.
	 *
	 * @param  string|null  $uuid
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_uuid( ?string $uuid ): void {
		$this->uuid = $uuid;
	}

	/**
	 * Get the creation timestamp.
	 *
	 * @return string|null
	 * @since 1.0.0
	 */
	public function get_created_at(): ?string {
		return $this->created_at;
	}

	/**
	 * Set the creation timestamp.
	 *
	 * @param  string|null  $created_at
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_created_at( ?string $created_at ): void {
		$this->created_at = $created_at;
	}

	/**
	 * Get the last update timestamp.
	 *
	 * @return string|null
	 * @since 1.0.0
	 */
	public function get_updated_at(): ?string {
		return $this->updated_at;
	}

	/**
	 * Set the last update timestamp.
	 *
	 * @param  string|null  $updated_at
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_updated_at( ?string $updated_at ): void {
		$this->updated_at = $updated_at;
	}

	/**
	 * Check if message was streamed.
	 *
	 * @return bool
	 * @since 1.0.0
	 */
	public function is_streamed(): bool {
		return $this->streamed;
	}

	/**
	 * Set streamed flag for message.
	 *
	 * @param  bool  $streamed
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_streamed( bool $streamed ): void {
		$this->streamed = $streamed;
	}

	/**
	 * Check if role is user.
	 *
	 * @return bool
	 * @since 1.0.0
	 */
	public function role_is_user(): bool {
		return $this->role === self::ROLE_USER;
	}

	/**
	 * Get all metas associated with this message.
	 *
	 * @return Message_Meta[]
	 * @since 1.0.0
	 */
	public function metas(): array {
		return Message_Meta::where( [ 'message_id' => $this->id ] )->get();
	}

	/**
	 * Get token usage associated with this message.
	 *
	 * @return Token_Usage|null
	 * @since 1.0.0
	 */
	public function get_usage(): ?Token_Usage {
		return $this->usage;
	}

	/**
	 * Set token usage for this message.
	 *
	 * @param  Token_Usage|null  $usage
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_usage( ?Token_Usage $usage ): void {
		$this->usage = $usage;
	}

	/**
	 * Extract all text parts from content and concatenate with new lines.
	 *
	 * @return string
	 * @since 1.0.0
	 */
	public function extract_text(): string {
		$text = '';
		foreach ( $this->get_content() as $part ) {
			if ( isset( $part['type'] ) && $part['type'] == 'text' && is_string( $part['text']['value'] ) ) {
				$text .= $part['text']['value'] . "\n";
			}
			if ( isset( $part['type'] ) && $part['type'] == self::CONTENT_TYPE_ACTION_SUBMISSION ) {
				$text .= $part[ self::CONTENT_TYPE_ACTION_SUBMISSION ]['message'] . "\n";
			}
		}

		return $text;
	}

	/**
	 * Get the content of this message.
	 *
	 * @return array|null
	 * @since 1.0.0
	 */
	public function get_content(): ?array {
		return $this->content;
	}

	/**
	 * Set the content of this message.
	 *
	 * @param  array|null  $content
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_content( ?array $content ): void {
		$this->content = $content;
	}

	/**
	 * Update or create a meta key/value pair for this message.
	 *
	 * If the meta key already exists for the current message, its value is updated.
	 * Otherwise, a new meta record is created.
	 *
	 * @param  string  $key  The meta key to update or create.
	 * @param  mixed  $value  The value to assign to the meta key.
	 *
	 * @return void
	 * @throws \Exception
	 * @since 1.0.0
	 */
	public function update_meta( $key, $value ) {
		if ( $this->get_meta( $key ) ) {
			Message_Meta::update( [ 'meta_key' => $key, 'message_id' => $this->id ], [ 'meta_value' => $value ] );
		} else {
			Message_Meta::create( [ 'message_id' => $this->id, 'meta_key' => $key, 'meta_value' => $value ] );
		}
	}

	/**
	 * Retrieve a meta object by key.
	 *
	 * @param  string  $meta_key
	 *
	 * @return Message_Meta|null
	 * @since 1.0.0
	 */
	public function get_meta( $meta_key ): ?Message_Meta {
		return Message_Meta::where( [ 'message_id' => $this->id, 'meta_key' => $meta_key ] )->first();
	}

	/**
	 * Retrieves the permalink or term link for the first valid dataset entry source.
	 *
	 * This method attempts to resolve a source link (either a post permalink or a term link)
	 * associated with the current object. It looks up dataset entry IDs stored in the
	 * meta field defined by `Knowledge_Source_Tracker::META_KEY_DATASET_ENTRY_SOURCES`.
	 *
	 * @return string|null The source URL (permalink or term link), or null if not found.
	 *
	 * @since 1.0.0
	 */
	public function source_link() {
		$dataset_entry_ids = $this->get_meta( Knowledge_Source_Tracker::META_KEY_DATASET_ENTRY_SOURCES );
		if (
			! $dataset_entry_ids instanceof Message_Meta
			|| empty( $dataset_entry_ids->get_meta_value() )
			|| ! is_array( $dataset_entry_ids->get_meta_value() )
			|| ! $this->role_is_assistant()
		) {
			return null;
		}
		$dataset_entry_ids = $dataset_entry_ids->get_meta_value();
		foreach ( $dataset_entry_ids as $dataset_entry_id ) {
			$dataset_entry = Dataset_Entry::find( $dataset_entry_id['id'] );
			if ( ! $dataset_entry instanceof Dataset_Entry ) {
				continue;
			}
			$dataset = $dataset_entry->dataset();
			if ( ! $dataset instanceof Dataset ) {
				continue;
			}
			if ( $url = $dataset->source_url() ) {
				return $url;
			}
		}

		return null;
	}

	/**
	 * Check if role is assistant.
	 *
	 * @return bool
	 * @since 1.0.0
	 */
	public function role_is_assistant(): bool {
		return $this->role === self::ROLE_ASSISTANT;
	}

	public function sources() {
		$sources           = [];
		$dataset_entry_ids = $this->get_meta( Knowledge_Source_Tracker::META_KEY_DATASET_ENTRY_SOURCES );
		$chatbot           = $this->get_chat()->get_chatbot();
		if ( $chatbot instanceof Chatbot ) {
			$count = $chatbot->get_parameter( 'kb_sources_section_items_count' );
		}
		$count = $count ?? self::DEFAULT_SOURCES_COUNT;
		if (
			! $dataset_entry_ids instanceof Message_Meta
			|| empty( $dataset_entry_ids->get_meta_value() )
			|| ! is_array( $dataset_entry_ids->get_meta_value() )
			|| ! $this->role_is_assistant()
		) {
			return null;
		}
		$dataset_entry_ids = $dataset_entry_ids->get_meta_value();
		foreach ( $dataset_entry_ids as $dataset_entry_id ) {
			$dataset_entry = Dataset_Entry::find( $dataset_entry_id['id'] );
			if ( ! $dataset_entry instanceof Dataset_Entry ) {
				continue;
			}
			$dataset = $dataset_entry->dataset();
			if ( ! $dataset instanceof Dataset ) {
				continue;
			}
			if ( $dataset->source_url() ) {
				$dataset = $dataset->with( 'source_url' );
				if ( ! in_array( $dataset, $sources ) ) {
					if ( $count > count( $sources ) ) {
						$sources[] = $dataset;
					} else {
						break;
					}
				}
			}
		}

		return $sources;
	}

	/**
	 * Get the Chat object this message belongs to.
	 *
	 * @return Chat|null
	 * @since 1.0.0
	 */
	public function get_chat(): ?Chat {
		return Chat::find_by_uuid( $this->get_chat_uuid() );
	}

	/**
	 * Get the chat UUID this message belongs to.
	 *
	 * @return string|null
	 * @since 1.0.0
	 */
	public function get_chat_uuid(): ?string {
		return $this->chat_uuid;
	}

	/**
	 * Set the chat UUID this message belongs to.
	 *
	 * @param  string|null  $chat_uuid
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_chat_uuid( ?string $chat_uuid ): void {
		$this->chat_uuid = $chat_uuid;
	}

	/**
	 * Retrieves the dataset entries associated with this object.
	 *
	 * This method looks up dataset entry IDs stored in the meta field
	 * defined by `Knowledge_Source_Tracker::META_KEY_DATASET_ENTRY_SOURCES`
	 * and returns the corresponding `Dataset_Entry` collection.
	 *
	 * @return Dataset_Entry[] A collection (array or ORM collection) of dataset entries.
	 *
	 * @since 1.0.0
	 */
	public function dataset_entries() {
		$ids               = [];
		$dataset_entry_ids = $this->get_meta( Knowledge_Source_Tracker::META_KEY_DATASET_ENTRY_SOURCES );
		if (
			! $dataset_entry_ids instanceof Message_Meta
			|| empty( $dataset_entry_ids->get_meta_value() )
			|| ! is_array( $dataset_entry_ids->get_meta_value() )
			|| ! $this->role_is_assistant()
		) {
			return [];
		}
		$dataset_entry_ids = $dataset_entry_ids->get_meta_value();
		foreach ( $dataset_entry_ids as $dataset_entry_id ) {
			$ids[] = $dataset_entry_id['id'];
		}

		return Dataset_Entry::where( [ 'id' => $ids ] )->get();
	}

	/**
	 * Get the user ID who sent this message.
	 *
	 * @return int|null
	 * @since 1.0.0
	 */
	public function get_user_id(): ?int {
		return $this->user_id;
	}

	/**
	 * Set the user ID who sent this message.
	 *
	 * @param  int|null  $user_id
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_user_id( ?int $user_id ): void {
		$this->user_id = $user_id;
	}

	public function inject_previous_message( Message $message ) {
		$this->included['previous_message'] = $message;
	}

	public function is_action_message() {
		return $this->is_action_message;
	}

	public function set_is_action_message( bool $value ) {
		$this->is_action_message = $value;
	}
	public function attach_agents( ?array $agents ) {
		$results = [];
		if ( ! empty( $agents ) ) {
			foreach ( $agents as $user ) {
				$controller = new WP_REST_Users_Controller();
				$request    = new WP_REST_Request( 'GET' );

				$response  = $controller->prepare_item_for_response( $user, $request );
				$results[] = $controller->prepare_response_for_collection( $response );
			}
		}
		$this->included['agents'] = $results;

		return $this;
	}

	/**
	 * Get agent data associated with this message.
	 *
	 * @return array|mixed|WP_REST_Response|null
	 */
	public function agent() {
		$agent_id = $this->get_meta( 'agent_id' );
		if ( ! $agent_id instanceof Message_Meta || empty( $agent_id->get_meta_value() ) ) {
			return null;
		}

		$user = get_user_by( 'ID', $agent_id->get_meta_value() );
		if ( ! $user ) {
			return null;
		}
		$controller = new WP_REST_Users_Controller();
		$request    = new WP_REST_Request( 'GET' );
		$response   = $controller->prepare_item_for_response( $user, $request );

		return $controller->prepare_response_for_collection( $response );
	}

	public function is_first_user_message() {
		// Must be a user role and have a valid chat UUID.
		if ( $this->get_role() !== self::ROLE_USER || ! $this->get_chat_uuid() ) {
			return false;
		}

		// Count persisted user messages for this chat. Because the current
		// message is already saved before reply generation, a count of 1
		// means this is the very first user message in the conversation.
		$count = static::count(
			[
				'chat_uuid' => $this->get_chat_uuid(),
				'role'      => self::ROLE_USER,
			]
		);

		return (int) $count === 1;
	}

	/**
	 * Get the role of this message.
	 *
	 * @return string|null
	 * @since 1.0.0
	 */
	public function get_role(): ?string {
		return $this->role;
	}

	/**
	 * Set the role of this message.
	 *
	 * @param  string|null  $role
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public function set_role( ?string $role ): void {
		$this->role = $role;
	}
}