<?php

namespace Limb_Chatbot\Includes\Services;

use Limb_Chatbot\Includes\Api\V1\Controllers\Chat_Messages_Controller;
use Limb_Chatbot\Includes\Data_Objects\Chat;
use Limb_Chatbot\Includes\Data_Objects\Chatbot;
use Limb_Chatbot\Includes\Data_Objects\Chatbot_User;
use Limb_Chatbot\Includes\Data_Objects\Message;
use Limb_Chatbot\Includes\Data_Objects\Message_Meta;
use Limb_Chatbot\Includes\Data_Objects\Widget;
use Limb_Chatbot\Includes\Database_Strategies\WPDB;
use Limb_Chatbot\Includes\Exceptions\Error_Codes;
use Limb_Chatbot\Includes\Exceptions\Exception;
use Limb_Chatbot\Includes\Repositories\Message_Repository;
use Limb_Chatbot\Includes\Services\Channel_Events\Channel_Event_Bootstrap;
use Limb_Chatbot\Includes\Services\Channel_Events\Channel_Event_Types;
use Limb_Chatbot\Includes\Services\Notifications\Notification_Bootstrap;
use Limb_Chatbot\Includes\Services\Notifications\Types\Chat_Created_Notification_Type;
use Limb_Chatbot\Includes\Services\Preview_State;
use Limb_Chatbot\Includes\Widgets\Data\Messages_Widget_Config;


/**
 * Service for managing chat messages.
 *
 * @since 1.0.0
 */
class Message_Service {

	/**
	 * The chat instance.
	 *
	 * @var null|Chat
	 * @since 1.0.0
	 */
	public ?Chat $chat;

	/**
	 * Message repository instance.
	 *
	 * @var Message_Repository|null
	 * @since 1.0.0
	 */
	public ?Message_Repository $repository;

	/**
	 * Constructor.
	 *
	 * @param  Chat|null  $chat  Chat instance.
	 * @param  Message_Repository|null  $repository  Optional message repository.
	 *
	 * @since 1.0.0
	 */
	public function __construct( ?Chat $chat, ?Message_Repository $repository = null ) {
		$this->chat       = $chat;
		$this->repository = $repository;
	}

	/**
	 * Creates a new message.
	 *
	 * @param array $data Message data.
	 * @param bool $preview Whether the message is a preview.
	 * @return Message|null Created message or null.
	 * @throws Exception|\Exception Throws if usage limits exceeded or preparation fails.
	 * @since 1.0.0
	 */
	public function create( array $data, bool $preview = false, $is_stream = false ): ?Message {
		$wpdb = new WPDB();
		$wpdb->start_transaction();
		try {
			$wpdb->lock_row( $this->chat );
			// Sanitize content before preparing it (safety check in case content didn't go through REST API)
			if ( ! empty( $data['content'] ) && is_array( $data['content'] ) ) {
				$messages_controller = new Chat_Messages_Controller();
				$data['content'] = $messages_controller->sanitize_content( $data['content'] );
			}
			$data['content']   = $this->prepare_content( $data['content'], $this->chat->get_chatbot() );
			$data['chat_uuid'] = $this->chat->get_uuid();
			$data['user_id']   = User_Manager::instance()->current_user->get_id();
			$message           = $this->repository->make( $data );
			$chatbot           = $this->chat->get_chatbot();
			if ( $preview && User_Manager::instance()->current_user->wp_can( 'manage_options' ) ) {
				$chatbot->set_preview( $preview );
				$preview_state = Preview_State::load();
				$chatbot->set_preview_state_parameters( $preview_state->get_parameters() );
			}
			// Check usage limits only if not in an ongoing action
			// (action parameter validation is handled in Action_Service::check_ongoing_action)
			if ( ! $this->chat->current_action_plan() ) {
				$usage_service = $chatbot->get_parameter( 'limits' ) instanceof Collection ? new Usage_Service( $chatbot->get_parameter( 'limits' ) ) : null;
				if ( $usage_service && $limit = $usage_service->limits_passed( $message ) ) {
					throw new Exception( Error_Codes::CHATBOT_LIMIT_EXCEED,
						$usage_service->get_error_message( $limit ) );
				}
				$widget_messages = $this->save_possible_assistant_messages();
				$message->included['widget_messages'] = $widget_messages;
			}
			$message->save();
			$this->chat->update_meta( 'admin_read', false );
			$this->set_chat_name( $message, $is_stream );
			$wpdb->commit_transaction();
			$this->chat->touch();

			return $message;
		} catch ( \Exception $e ) {
			$wpdb->rollback_transaction();
			throw $this->format_error( $e );
		}
	}

	/**
	 * Saves assistant messages if none exist.
	 *
	 * @return void
	 * @throws \Exception
	 * @since 1.0.0
	 */
	protected function save_possible_assistant_messages(): array {
		if ( ! $this->chat->get_messages_count() ) {
			$content = [];
			$widgets = null;
			$widgets_override = false;

			// Try preview widgets from preview state when chat is marked as preview.
			if ( ! empty( $this->chat->get_meta( 'preview' ) ) ) {
				$preview_state = Preview_State::load();
				$widgets_data  = $preview_state->get_widgets();
				$widgets_override = $preview_state->widgets_override_enabled();
				if ( is_array( $widgets_data ) && ! empty( $widgets_data ) ) {
					$config = new Messages_Widget_Config();
					foreach ( $widgets_data as $widget_item ) {
						if ( $widget_item instanceof Widget ) {
							$widget_obj = $widget_item;
						} elseif ( is_array( $widget_item ) ) {
							// Preview state stores sanitized widget payload arrays.
							$widget_obj = new Widget( $widget_item );
						} else {
							continue;
						}

						if ( ! $widget_obj->is_for_new_chat() || ! $widget_obj->is_visible_for_current_user() ) {
							continue;
						}

						$content = array_merge( $content, $config->extract_content( $widget_obj ) );
					}
				}
			}

			// Fallback to DB widgets when no preview widgets are available.
			// If admin explicitly overridden widgets in preview state, do not fallback.
			if ( empty( $content ) && ( empty( $widgets_override ) || ! $widgets_override ) ) {
				$widgets = Widget::where( [ 'type' => 'messages', 'published' => 1 ], - 1, 1, '`order`', 'ASC' );
				if ( ! $widgets->is_empty() ) {
					$config = new Messages_Widget_Config();
					foreach ( $widgets->get() as $widget_item ) {
						if ( ! $widget_item instanceof Widget ) {
							continue;
						}
						if ( ! $widget_item->is_for_new_chat() ) {
							continue;
						}
						if ( ! $widget_item->is_visible_for_current_user() ) {
							continue;
						}

						$content = array_merge( $content, $config->extract_content( $widget_item ) );
					}
				}
			}

			if ( ! empty( $content ) ) {
				$new_assistant_message = new Message();
				$new_assistant_message->set_role( Message::ROLE_ASSISTANT );
				$new_assistant_message->set_content( $content );
				$new_assistant_message->set_chat_uuid( $this->chat->get_uuid() );
				$new_assistant_message->save();
				$result[] = $new_assistant_message;
			}
		}

		return $result ?? [];
	}

	/**
	 * Retrieves message items according to parameters.
	 *
	 * @param mixed $params Query parameters.
	 * @return Collection|null Collection of messages or null.
	 * @since 1.0.0
	 */
	public function get_items( $params ): ?Collection {
		return $this->repository->get_items( $this->chat, $params );
	}

	/**
	 * Prepares message content, uploading attachments if needed.
	 *
	 * @param mixed $content The content to prepare.
	 * @param Chatbot $chatbot Chatbot instance.
	 * @return string JSON encoded prepared content.
	 * @throws Exception Throws if content preparation fails.
	 * @since 1.0.0
	 */
	public function prepare_content( $content, Chatbot $chatbot ): string {
		// Media attachment storage scheme is enabled, then we should upload the image into the media
		$content = wp_json_encode( $content );
		if ( empty( $content ) ) {
			throw new Exception( Error_Codes::USER_MESSAGE_PREPARATION_FAILED, __( 'Array message failed', 'limb-chatbot' ) );
		}

		return $content;
	}

	/**
	 * Sets the chat name based on the first user text message.
	 *
	 * @param  Message|null  $message  The message to extract name from.
	 *
	 * @return void
	 * @throws \Exception
	 * @since 1.0.0
	 */
	protected function set_chat_name( ?Message $message, $is_stream = false ): void {
		if ( empty( $message ) || $this->chat->get_name() || ! $message->role_is_user() ) {
			return;
		}
		$content = Helper::maybe_json_decode( $message->get_content() );
		foreach ( $content as $item ) {
			if ( $item['type'] === 'text' ) {
				$this->chat->set_name( $item['text']['value'] );
				$this->chat->save();
				break;
			}
		}

		$chat_uuid = $this->chat->get_uuid();

		// Do not dispatch notifications for preview chats.
		// 1) Always create a channel event row for *website admins only*.
		if ( empty( $this->chat->get_meta( 'preview' ) ) ) {
			$admin_chatbot_users = User_Manager::instance()->get_admins();
			$admin_wp_user_ids   = [];
			foreach ( $admin_chatbot_users as $admin ) {
				$wp_user_id = $admin->get_wp_user_id();
				if ( $wp_user_id !== null ) {
					$admin_wp_user_ids[] = (int) $wp_user_id;
				}
			}
			$admin_wp_user_ids = array_values( array_unique( array_filter( $admin_wp_user_ids ) ) );

			$chatbot_user = User_Manager::instance()->get_current_user();
			$chatbot_user_uuid = $chatbot_user->uuid ?? null;

			if ( ! empty( $admin_wp_user_ids ) && $chat_uuid !== '' ) {
				Channel_Event_Bootstrap::get_dispatcher()->dispatch(
					Channel_Event_Types::ADMIN_NEW_CHAT,
					$admin_wp_user_ids,
					$chat_uuid,
					$chatbot_user_uuid,
					[ 'chat_uuid' => $chat_uuid ]
				);
			}

			Notification_Bootstrap::get_service()->queue(
				$chatbot_user,
				Chat_Created_Notification_Type::TYPE_KEY,
				[ 'chat_uuid' => $chat_uuid ]
			);
		}
		if ( $is_stream ) {
			Stream_Event_Service::chat_created( $this->chat );
		}
	}

	/**
	 * Deletes messages and associated metas by criteria.
	 *
	 * @param mixed $where Conditions to find messages to delete.
	 * @return bool True if delete succeeded, false otherwise.
	 * @since 1.0.0
	 */
	public function delete( $where ): bool {
		$messages = Message::where( $where )->get();
		$delete   = $this->repository->delete( $where );
		if ( $delete ) {
			foreach ( $messages as $message ) {
				$message_id = $message->get_id();
				if ( $message_id ) {
					Message_Meta::delete( [ 'message_id' => $message_id ] );
				}
			}
		}

		return $delete;
	}

	protected function format_error( \Exception $e ): \Exception {
		$error = Helper::get_wp_error( $e );
		if ( $code = $e->getCode() ) {
			if ( in_array( $code, [ Error_Codes::CHATBOT_LIMIT_EXCEED, Error_Codes::USER_MESSAGE_LIMIT_EXCEED ] ) ) {
				return $e;
			}
		}

		return new Exception( Error_Codes::USER_MESSAGE_FAILED, $error->get_error_message() );
	}
}