<?php

/**
 * Send message to AI as administrator
 */
final class VAPPV_TTW_Chat_Messages {

	/* --------------------------------------------------------------- */
	/*                        Public functions                         */
	/* --------------------------------------------------------------- */

	/**
	 * Send a message to the AI as a regular website visitor.
	 *
	 * @param string $user_name User name
	 * @param string $user_email User Email
	 * @param string $user_guid User GUID
	 * @param string $message_guid Message GUID
	 * @param string $message_text Message text
	 *
	 * @return array
	 * @throws Exception
	 */
	public function send_message_to_ai_as_user(
		string $user_name,
		string $user_email,
		string $user_guid,
		string $message_guid,
		string $message_text
	): array {

		global $wpdb;

		//Create controllers
		$settings_controller = new VAPPV_TTW_Settings();
		$settings_controller->load_all_saved_settings();

		//Check is the plugin active right now
		$plugin_activity_info = $settings_controller->is_plugin_active_right_now();
		if ( false === $plugin_activity_info['IsActive'] ) {
			return VAPPV_TTW_Core::ajax_pack_error( 'PLUGIN_IS_NOT_ACTIVE', $plugin_activity_info );
		}

		//----------------------------------------------------------------
		//Sanitize message max length according to setting (message length limit)
		$settings_max_characters_per_user_question = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_max_characters_per_user_question );
		if ( mb_strlen( $message_text, 'utf-8' ) > $settings_max_characters_per_user_question ) {
			$message_text = mb_substr( $message_text, 0, $settings_max_characters_per_user_question, 'utf-8' );
		}

		//Sanitize other fields length again (after WP sanitizing functions)
		if ( mb_strlen( $user_name, 'utf-8' ) > 50 ) {
			$user_name = mb_substr( $user_name, 0, 50, 'utf-8' );
		}
		if ( mb_strlen( $user_email, 'utf-8' ) > 50 ) {
			$user_email = mb_substr( $user_email, 0, 50, 'utf-8' );
		}
		if ( mb_strlen( $user_guid, 'utf-8' ) > 40 ) {
			$user_guid = mb_substr( $user_guid, 0, 40, 'utf-8' );
		}
		if ( mb_strlen( $message_guid, 'utf-8' ) > 40 ) {
			$message_guid = mb_substr( $message_guid, 0, 40, 'utf-8' );
		}
		//----------------------------------------------------------------

		//------------------------------------------------
		//Check daily limits
		$ttw_stats_controller = new VAPPV_TTW_Statistics();
		$can_we_process_message_based_on_daily_limits = $ttw_stats_controller->can_process_message_from_user( $settings_controller, $message_text );
		if ( false === $can_we_process_message_based_on_daily_limits ) {
			return VAPPV_TTW_Core::ajax_pack_error(
				'DAILY_CONVERSATION_LIMIT_EXCEEDED',
				esc_html(
					/* translators: plugin exception = limit of communication with the AI has been exceeded */
					__( 'The daily limit of communication with the AI has been exceeded, based on the plugin settings.', 'talk-to-website' )
				)
			);
		}
		//------------------------------------------------

		//Create ChatGPT controller
		$openai_api_address = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_openai_api_address );
		$openai_api_key = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_openai_api_key );
		$ttw_openai_api_controller = new VAPPV_TTW_OpenAI_API_Connector( $openai_api_address, $openai_api_key );

		//Get current assistant data from DB
		$chatgpt_current_assistant_data_raw = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_current_assistant_data );
		if ( is_null( $chatgpt_current_assistant_data_raw ) ) {
			//No assistant associated. Throw an exception.
			return VAPPV_TTW_Core::ajax_pack_error(
				'CHATGPT_ASSISTANT_NOT_CREATED_YET',
				esc_html(
					/* translators: plugin exception = failed to find assistant data */
					__( 'Failed to find assistant data. Please re-train the plugin.', 'talk-to-website' )
				)
			);
		}
		$chatgpt_current_assistant_data_object = json_decode( $chatgpt_current_assistant_data_raw, true );
		$chatgpt_assistant_id = $chatgpt_current_assistant_data_object['id'];

		//Extract data from the local DB
		$db_tables_prefix = $wpdb->prefix;
		$wp_ttw_dialogues_table_name = "{$db_tables_prefix}vappv_ttw_dialogues";
		$wp_ttw_dialogues_messages_table_name = "{$db_tables_prefix}vappv_ttw_dialogues_messages";
		$settings_dialogue_lifetime_minutes = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_dialogue_lifetime_minutes );
		$settings_dialogue_lifetime_max_messages = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_dialogue_lifetime_max_messages );
		$current_date_time = new DateTime( gmdate( 'Y-m-d H:i:s' ) );
		$max_old_date_time_for_thread = null;

		if ( -1 !== $settings_dialogue_lifetime_minutes ) {
			$max_old_date_time_for_thread = new DateTime( gmdate( 'Y-m-d H:i:s' ) );
			$max_old_date_time_for_thread->sub( new DateInterval( "PT{$settings_dialogue_lifetime_minutes}M" ) );
		}

		//#1 Extract dialogue information or create a new dialogue
		$dialogue_data = null;
		$current_user_dialogue_from_wpdb = array();
		if ( -1 !== $settings_dialogue_lifetime_minutes ) {
			$current_user_dialogue_from_wpdb = $wpdb->get_results(
				$wpdb->prepare( "SELECT * FROM %i WHERE `is_thread_by_admin`=%d AND `user_guid`=%s AND `start_date`>=%s ORDER BY `id` DESC LIMIT 0, 1", array(
					$wp_ttw_dialogues_table_name,
					0,
					$user_guid,
					$max_old_date_time_for_thread->format( 'Y-m-d H:i:s' )
				) ),
				ARRAY_A
			);
		} else {
			$current_user_dialogue_from_wpdb = $wpdb->get_results(
				$wpdb->prepare( "SELECT * FROM %i WHERE `is_thread_by_admin`=%d AND `user_guid`=%s ORDER BY `id` DESC LIMIT 0, 1", array(
					$wp_ttw_dialogues_table_name,
					0,
					$user_guid
				) ),
				ARRAY_A
			);
		}

		//Check extracted thread object
		$should_restart_thread_by_messages_count_limit = false;
		$extracted_thread_assistant_id = null;
		if (
			! is_null( $current_user_dialogue_from_wpdb ) &&
			count( $current_user_dialogue_from_wpdb ) > 0
		) {

			//Get thread assistant id
			$extracted_thread_assistant_id = $current_user_dialogue_from_wpdb[0]['ai_assistant_id'];

			//---------------------------------
			//Check thread messages limit

			//Get thread current messages count
			if ( -1 !== $settings_dialogue_lifetime_max_messages ) {
				$current_dialogue_messages_count_from_wpdb_rows = $wpdb->get_results( $wpdb->prepare( "SELECT COUNT(*) AS `thread_messages_count` FROM %i WHERE `dialogue_id`=%d", array( $wp_ttw_dialogues_messages_table_name, $current_user_dialogue_from_wpdb[0]['id'] ) ), ARRAY_A );
				if ( $current_dialogue_messages_count_from_wpdb_rows != null && count( $current_dialogue_messages_count_from_wpdb_rows ) > 0 ) {
					$dialogue_messages_count = $current_dialogue_messages_count_from_wpdb_rows[0]['thread_messages_count'];

					//Compare messages count and limit
					if ( $dialogue_messages_count > $settings_dialogue_lifetime_max_messages ) {
						$should_restart_thread_by_messages_count_limit = true;
					}
				}
			}
			//---------------------------------
		}

		//Check results and process data
		$is_new_dialogue_started = false;
		if (
			is_null( $current_user_dialogue_from_wpdb ) ||
			0 === count( $current_user_dialogue_from_wpdb ) ||
			$extracted_thread_assistant_id !== $chatgpt_assistant_id ||
			true === $should_restart_thread_by_messages_count_limit
		) {
			//Create new thread into ChatGPT and extract its data
			$chatgpt_create_thread_results = $ttw_openai_api_controller->create_thread();
			if ( ! is_array( $chatgpt_create_thread_results ) ) {
				if (
					'VAPPV_TTW_OpenAI_API_Error' === get_class( $chatgpt_create_thread_results ) ||
					'VAPPV_TTW_Common_Error' === get_class( $chatgpt_create_thread_results )
				) {
					return VAPPV_TTW_Core::ajax_pack_error(
						'CHATGPT_CREATE_THREAD_ERROR',
						sprintf(
							'%1$s // %2$s',
							esc_html(
								/* translators: plugin exception = failed to create thread into ChatGPT via OpenAI API */
								__( 'Failed to create ChatGPT thread', 'talk-to-website' )
							),
							esc_html(
								$chatgpt_create_thread_results->message
							)
						)
					);
				}
			}

			//Save data into DB
			$dialogue_data = array(
				'start_date'           => gmdate( 'Y-m-d H:i:s' ),
				'user_name'            => $user_name,
				'user_email'           => $user_email,
				'user_guid'            => $user_guid,
				'user_ip'              => VAPPV_TTW_Common::get_client_ip(),
				'is_thread_by_admin'   => 0,
				'ai_thread_id'         => $chatgpt_create_thread_results['id'],
				'ai_assistant_id'      => $chatgpt_assistant_id,
				'ai_thread_all_params' => wp_json_encode( $chatgpt_create_thread_results )
			);
			$wpdb->insert(
				$wp_ttw_dialogues_table_name,
				$dialogue_data
			);
			$lastInsertedEntryId = $wpdb->insert_id;
			$dialogue_data['id'] = $lastInsertedEntryId;
			$is_new_dialogue_started = true;

		} else {
			//Extract existing dialogue data
			$dialogue_data = $current_user_dialogue_from_wpdb[0];
		}

		//Deal with the dialogue
		//#2 Create message to the thread
		$chatgpt_create_new_message_results = $ttw_openai_api_controller->create_message( $dialogue_data["ai_thread_id"], $message_text );
		if ( ! is_array( $chatgpt_create_new_message_results ) ) {
			if (
				'VAPPV_TTW_OpenAI_API_Error' === get_class( $chatgpt_create_new_message_results ) ||
				'VAPPV_TTW_Common_Error' === get_class( $chatgpt_create_new_message_results )
			) {
				return VAPPV_TTW_Core::ajax_pack_error(
					'CHATGPT_CREATE_MESSAGE_ERROR',
					sprintf(
						'%1$s // %2$s',
						esc_html(
							/* translators: plugin exception = failed to create message into ChatGPT via OpenAI API */
							__( 'Failed to create ChatGPT message', 'talk-to-website' )
						),
						esc_html(
							$chatgpt_create_new_message_results->message
						)
					)
				);
			}
		}

		//Get settings for run
		$chatgpt_current_temperature = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_chatgpt_temperature );

		//#3 Create and launch the run
		$chatgpt_create_new_run_results = $ttw_openai_api_controller->create_run( $dialogue_data["ai_thread_id"], $chatgpt_assistant_id, $chatgpt_current_temperature, $settings_dialogue_lifetime_max_messages, false );
		if ( ! is_array( $chatgpt_create_new_run_results ) ) {
			if (
				'VAPPV_TTW_OpenAI_API_Error' === get_class( $chatgpt_create_new_run_results ) ||
				'VAPPV_TTW_Common_Error' === get_class( $chatgpt_create_new_run_results )
			) {
				return VAPPV_TTW_Core::ajax_pack_error(
					'CHATGPT_CREATE_RUN_ERROR',
					sprintf(
						'%1$s // %2$s',
						esc_html(
							/* translators: plugin exception = failed to create run into ChatGPT via OpenAI API */
							__( 'Failed to create ChatGPT run', 'talk-to-website' )
						),
						esc_html(
							$chatgpt_create_new_run_results->message
						)
					)
				);
			}
		}

		//#4 Save all necessary data into DB and return packed object
		//Save data into DB
		$message_data = array(
			'dialogue_id'               => $dialogue_data['id'],
			'related_user_guid'         => $user_guid,
			'sender_role'               => 'user',
			'message_type'              => 'message_by_visitor',
			'message_guid'              => $message_guid,
			'message_text'              => $message_text,
			'message_date'              => gmdate( 'Y-m-d H:i:s' ),
			'response_date'             => null,
			'ai_message_id'             => $chatgpt_create_new_message_results['id'],
			'ai_thread_id'              => $dialogue_data['ai_thread_id'],
			'ai_run_id'                 => $chatgpt_create_new_run_results['id'],
			'is_responded_by_ai'        => null,
			'is_executed_with_error'    => null,
			'is_read_by_sender'         => null,
			'rate_by_sender'            => null,
			'is_url_followed_by_sender' => null
		);

		$wpdb->insert(
			$wp_ttw_dialogues_messages_table_name,
			$message_data
		);
		$lastInsertedEntryId = $wpdb->insert_id;
		$message_data['id'] = $lastInsertedEntryId;

		//-----------------------------------------
		//Add stats data
		$dialogues_count = ( $is_new_dialogue_started == true ? 1 : 0 );
		$income_words_count = count( explode( ' ', $message_text ) );
		$income_characters_count = mb_strlen( $message_text, 'utf-8' );
		$ttw_stats_controller->add_stats( $dialogues_count, 1, $income_words_count, $income_characters_count, 0, 0, 0 );
		//-----------------------------------------

		//Pack results
		return VAPPV_TTW_Core::ajax_pack_success( array(
			'MessageResponse' => esc_html(
				/* translators: message to OpenAI assistant was sent via API */
				__( 'Message has been sent, waiting for a response from AI. Please check the results after some time.', 'talk-to-website' )
			),
			'InsertedMessage' => $message_data
		) );
	}

	/**
	 * Try extract awaiting responses from AI as admin. Get results as JSON.
	 *
	 * @param string $user_guid User GUID
	 *
	 * @return array
	 * @throws Exception
	 */
	public function try_extract_response_from_ai_as_user( string $user_guid ): array {

		global $wpdb;

		//Sanitize length of the user GUID
		if ( mb_strlen( $user_guid, 'utf-8' ) > 40 ) {
			$user_guid = mb_substr( $user_guid, 0, 40, 'utf-8' );
		}

		//Create controllers
		$settings_controller = new VAPPV_TTW_Settings();
		$settings_controller->load_all_saved_settings();

		//Check is the plugin active right now
		$plugin_activity_info = $settings_controller->is_plugin_active_right_now();
		if ( false === $plugin_activity_info['IsActive'] ) {
			return VAPPV_TTW_Core::ajax_pack_error( 'PLUGIN_IS_NOT_ACTIVE', $plugin_activity_info );
		}

		//Create ChatGPT controller
		$openai_api_address = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_openai_api_address );
		$openai_api_key = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_openai_api_key );
		$ttw_openai_api_controller = new VAPPV_TTW_OpenAI_API_Connector( $openai_api_address, $openai_api_key );

		//Get current assistant data from DB
		$chatgpt_current_assistant_data_raw = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_current_assistant_data );
		if ( is_null( $chatgpt_current_assistant_data_raw ) ) {
			//No assistant associated. Throw an exception.
			return VAPPV_TTW_Core::ajax_pack_error(
				'CHATGPT_ASSISTANT_NOT_CREATED_YET',
				esc_html(
					/* translators: plugin exception = failed to find assistant data, assistant re-training required */
					__( 'Failed to find assistant data. Please re-train the plugin.', 'talk-to-website' )
				)
			);
		}
		$chatgpt_current_assistant_data_object = json_decode( $chatgpt_current_assistant_data_raw, true );
		$chatgpt_assistant_id = $chatgpt_current_assistant_data_object['id'];

		//Extract data from the local DB
		$db_tables_prefix = $wpdb->prefix;
		$wp_ttw_dialogues_table_name = "{$db_tables_prefix}vappv_ttw_dialogues";
		$wp_ttw_dialogues_messages_table_name = "{$db_tables_prefix}vappv_ttw_dialogues_messages";

		//Try to get user conversation thread
		$current_user_dialogue_from_wpdb = $wpdb->get_results(
			$wpdb->prepare( "SELECT * FROM %i WHERE `is_thread_by_admin`=%d AND `user_guid`=%s ORDER BY `id` DESC LIMIT 0, 1", array(
				$wp_ttw_dialogues_table_name,
				0,
				$user_guid
			) ),
			ARRAY_A
		);

		if (
			is_null( $current_user_dialogue_from_wpdb )||
			0 === count( $current_user_dialogue_from_wpdb )
		) {
			return VAPPV_TTW_Core::ajax_pack_error(
				'USER_NO_THREADS_YET',
				esc_html(
					/* translators: plugin exception = no user dialogues were found */
					__( 'No dialogs matching this user were found.', 'talk-to-website' )
				)
			);
		}
		$current_user_dialogue_wpdb_id = $current_user_dialogue_from_wpdb[0]['id'];

		//Try to get unanswered messages from admin
		$user_pending_messages_from_wpdb = $wpdb->get_results(
			$wpdb->prepare( "SELECT * FROM %i WHERE `dialogue_id`=%d AND `sender_role`=%s AND `message_type`=%s AND `is_responded_by_ai` IS NULL AND `is_executed_with_error` IS NULL ORDER BY `id` DESC LIMIT 0, 1", array(
				$wp_ttw_dialogues_messages_table_name,
				$current_user_dialogue_wpdb_id,
				'user',
				'message_by_visitor'
			) ),
			ARRAY_A
		);

		if (
			is_null( $user_pending_messages_from_wpdb ) || 
			0 === count( $user_pending_messages_from_wpdb )
		) {
			return VAPPV_TTW_Core::ajax_pack_success( array(
				'ExtractionResults' => 'no_new_messages',
				'Response'          => esc_html(
					/* translators: no unanswered messaged found into dialogue */
					__( 'No queued (unanswered) messages found', 'talk-to-website' )
				)
			) );
		}

		//Get unanswered message
		$pending_message_data = $user_pending_messages_from_wpdb[0];

		//#1 Try to resolve ChatGPT run
		$chatgpt_retrieve_run_results = $ttw_openai_api_controller->retrieve_run( $pending_message_data['ai_thread_id'], $pending_message_data['ai_run_id'] );
		if ( ! is_array( $chatgpt_retrieve_run_results ) ) {
			if (
				'VAPPV_TTW_OpenAI_API_Error' === get_class( $chatgpt_retrieve_run_results ) ||
				'VAPPV_TTW_Common_Error' === get_class( $chatgpt_retrieve_run_results )
			) {
				return VAPPV_TTW_Core::ajax_pack_error(
					'CHATGPT_RETRIEVE_RUN_ERROR',
					sprintf(
						'%1$s // %2$s',
						esc_html(
							/* translators: plugin exception = failed to retrieve run from ChatGPT via OpenAI API */
							__( 'Failed to retrieve run from ChatGPT.', 'talk-to-website' )
						),
						esc_html(
							$chatgpt_retrieve_run_results->message
						)
					)
				);
			}
		}

		//Analyse run results
		//Check for failed runs
		if (
			'failed'            === $chatgpt_retrieve_run_results['status'] ||
			'cancelled'         === $chatgpt_retrieve_run_results['status'] ||
			'cancelling'        === $chatgpt_retrieve_run_results['status'] ||
			'expired'           === $chatgpt_retrieve_run_results['status'] ||
			'required_action'   === $chatgpt_retrieve_run_results['status']
		) {
			//Failed to answer user message. Execute response as failed.
			$occured_error_details = $chatgpt_retrieve_run_results['last_error'];
			$wpdb->update(
				$wp_ttw_dialogues_messages_table_name,
				array(
					'is_executed_with_error' => 1,
					'ai_error_code'          => esc_html( "{$chatgpt_retrieve_run_results['status']}/{$occured_error_details["code"]}" ),
					'ai_error_details'       => $occured_error_details['message'],
					'response_date'          => gmdate( 'Y-m-d H:i:s' )
				),
				array(
					'id' => $pending_message_data['id']
				)
			);

			//Return results and stop execution
			return VAPPV_TTW_Core::ajax_pack_success( array(
				'ExtractionResults' => 'last_message_executed_with_error',
				'ErrorCode'         => $occured_error_details['code'],
				'ErrorDetails'      => $occured_error_details['message'],
				'MessageId'         => $pending_message_data->id,
				'Response'          => esc_html(
					/* translators: plugin exception = chatgpt thrown an error during message execution */
					__( 'ChatGPT thrown an error during message execution.', 'talk-to-website' )
				)
			) );
		}

		//Check for processing / queued runs
		if (
			'queued'        === $chatgpt_retrieve_run_results['status'] ||
			'in_progress'   === $chatgpt_retrieve_run_results['status']
		) {
			//Answer is not ready yet
			return VAPPV_TTW_Core::ajax_pack_success( array(
				'ExtractionResults' => 'ai_answer_is_still_pending',
				'Response'          =>
					sprintf(
						'%1$s // %2$s',
						esc_html(
							/* translators: the response from the ChatGPT is not yet ready */
							__( 'The response from the AI is not yet ready.', 'talk-to-website' )
						),
						esc_html( "(status = {$chatgpt_retrieve_run_results['status']})" )
					)
			) );
		}

		//Prepare usage stats containers
		$usageStats              = null;
		$ai_execution_stats_json = null;

		//Check for completed runs
		if ( 'completed' === $chatgpt_retrieve_run_results['status'] ) {

			//Run is completed successfully
			//Get usage stats
			$usageStats = $chatgpt_retrieve_run_results['usage'];
			$ai_execution_stats_json = wp_json_encode( $usageStats );

			$wpdb->update(
				$wp_ttw_dialogues_messages_table_name,
				array(
					'is_responded_by_ai' => 1,
					'response_date'      => gmdate( 'Y-m-d H:i:s' )
				),
				array(
					'id' => $pending_message_data['id']
				)
			);
		}

		//#2 Fetch new messages from ChatGPT if success
		$chatgpt_get_messages_results = $ttw_openai_api_controller->list_messages( $pending_message_data['ai_thread_id'], $pending_message_data['ai_message_id'], 20 );
		if ( ! is_array( $chatgpt_get_messages_results ) ) {
			if (
				'VAPPV_TTW_OpenAI_API_Error' === get_class( $chatgpt_get_messages_results ) ||
				'VAPPV_TTW_Common_Error' === get_class( $chatgpt_get_messages_results )
			) {
				return VAPPV_TTW_Core::ajax_pack_error(
					'CHATGPT_RETRIEVE_RUN_MESSAGES_ERROR',
					sprintf(
						'%1$s // %2$s',
						esc_html(
							/* translators: plugin exception = failed to get run messages from ChatGPT via OpenAI API */
							__( 'Failed to get run messages from ChatGPT.', 'talk-to-website' )
						),
						esc_html(
							$chatgpt_get_messages_results->message
						)
					)
				);
			}
		}

		//Prepare containers
		$messages_data = $chatgpt_get_messages_results['data'];
		$messages_packed_results_ttw = array();

		//Prepare counters
		$output_words_count_total = 0;
		$output_characters_count_total = 0;
		$output_tokens_count_total = 0;

		//Increment completion tokens count
		if (
			! is_null( $usageStats ) && 
			isset( $usageStats['completion_tokens'] )
		) {
			$output_tokens_count_total += $usageStats['completion_tokens'];
		}

		//Itterate answers (maybe many messages)
		for ( $aim = 0; $aim < count( $messages_data ); $aim++ ) {

			//Skip messages from user (copy of already stored messages)
			if ( isset( $messages_data[ $aim ]['metadata'] ) ) {
				if ( isset( $messages_data[ $aim ]['metadata']['is_message_from_user'] ) ) {
					if (
						'1' === $messages_data[ $aim ]['metadata']['is_message_from_user'] ||
						1 === (int)$messages_data[ $aim ]['metadata']['is_message_from_user']
					) {
						//This is a copy of user message. Just keep working.
						continue;
					}
				}
			}

			//Operated answers from AI
			$next_message_response_full_text = '';
			$message_content = $messages_data[ $aim ]['content'];
			for ( $mc = 0; $mc < count( $message_content ); $mc++ ) {
				if ( 'text' === $message_content[ $mc ]['type'] ) {
					//Get text part of the answer
					$next_content_part = $message_content[ $mc ]['text']['value'];

					//Check for citation (annotations and quotes from retrieval)
					if ( isset( $message_content[ $mc ]['text']['annotations'] ) ) {
						if ( is_array( $message_content[ $mc ]['text']['annotations'] ) ) {
							for ( $ac = 0; $ac < count( $message_content[ $mc ]['text']['annotations'] ); $ac++ ) {
								$next_annotation = $message_content[ $mc ]['text']['annotations'][ $ac ];
								if ( 'file_citation' === $next_annotation['type'] ) {

									$text_to_be_replaced = $next_annotation['text'];
									$replacement = '';
									//$replacement = $next_annotation['file_citation']['quote'];

									if ( false !== strpos( $next_content_part, $text_to_be_replaced ) ) {
										$next_content_part = str_replace( $text_to_be_replaced, $replacement, $next_content_part );
									}
								}
							}
						}
					}

					//Add content part to the whole ansert
					if ( 0 === $mc ) {
						$next_message_response_full_text .= $next_content_part;
					} else {
						$next_message_response_full_text .= ( '; ' . $next_content_part );
					}
				}
			}

			//Increment stats
			if ( '' !== $next_message_response_full_text ) {
				$output_words_count_total += count( explode( ' ', $next_message_response_full_text ) );
				$output_characters_count_total += mb_strlen( $next_message_response_full_text, 'utf-8' );
			}

			//Insert new message to DB
			//Save data into DB
			$message_data = array(
				'dialogue_id'               => $pending_message_data['dialogue_id'],
				'related_user_guid'         => $pending_message_data['related_user_guid'],
				'sender_role'               => 'ai',
				'message_type'              => 'response_for_visitor',
				'message_guid'              => VAPPV_TTW_Core::create_guid(),
				'message_text'              => $next_message_response_full_text,
				'message_date'              => gmdate( 'Y-m-d H:i:s' ),
				'response_date'             => null,
				'ai_message_id'             => $messages_data[ $aim ]['id'],
				'ai_thread_id'              => $messages_data[ $aim ]['thread_id'],
				'ai_run_id'                 => $messages_data[ $aim ]['run_id'],
				'is_responded_by_ai'        => null,
				'is_executed_with_error'    => null,
				'is_read_by_sender'         => null,
				'rate_by_sender'            => null,
				'is_url_followed_by_sender' => null,
				'ai_execution_stats'        => $ai_execution_stats_json
			);
			$wpdb->insert(
				$wp_ttw_dialogues_messages_table_name,
				$message_data
			);
			$lastInsertedEntryId = $wpdb->insert_id;
			$message_data['id']  = $lastInsertedEntryId;
			array_push( $messages_packed_results_ttw, $message_data );
		}

		//-----------------------------------------
		//Add stats data
		$ttw_stats_controller = new VAPPV_TTW_Statistics();
		$ttw_stats_controller->add_stats( 0, 1, 0, 0, $output_words_count_total, $output_characters_count_total, $output_tokens_count_total );

		//-----------------------------------------

		return VAPPV_TTW_Core::ajax_pack_success( array(
			'ExtractionResults' => 'messages_are_extracted',
			'Response'          => esc_html(
				/* translators: responses from ChatGPT extracted and recorded in the db */
				__( 'Responses from ChatGPT extracted and recorded in the database.', 'talk-to-website' )
			),
			'Messages'          => $messages_packed_results_ttw
		) );

	}

	/**
	 * List testing thread messages as admin. Get results as JSON.
	 *
	 * @param string $user_guid User GUID
	 * @param string $data_direction Direction of the data extraction { "backward" || "forward" }
	 * @param integer $last_known_id Last known message ID
	 * @param integer $data_limit Limit of data to extract
	 *
	 * @return array
	 * @throws Exception
	 */
	public function list_all_chat_messages_as_user(
		string $user_guid,
		string $data_direction,
		int $last_known_id,
		int $data_limit
	): array {

		global $wpdb;

		//Sanitize length of the user GUID
		if ( mb_strlen( $user_guid, 'utf-8' ) > 40 ) {
			$user_guid = mb_substr( $user_guid, 0, 40, 'utf-8' );
		}

		//Create controllers
		$settings_controller = new VAPPV_TTW_Settings();
		$settings_controller->load_all_saved_settings();

		//Check is the plugin active right now
		$plugin_activity_info = $settings_controller->is_plugin_active_right_now();
		if ( false === $plugin_activity_info['IsActive'] ) {
			return VAPPV_TTW_Core::ajax_pack_error( 'PLUGIN_IS_NOT_ACTIVE', $plugin_activity_info );
		}

		//Extract data from the local DB
		$db_tables_prefix = $wpdb->prefix;
		$wp_ttw_dialogues_table_name = "{$db_tables_prefix}vappv_ttw_dialogues";
		$wp_ttw_dialogues_messages_table_name = "{$db_tables_prefix}vappv_ttw_dialogues_messages";

		//Simple check for at least one user thread
		$current_user_dialogue_from_wpdb = $wpdb->get_results(
			$wpdb->prepare( "SELECT * FROM %i WHERE `is_thread_by_admin`=%d AND `user_guid`=%s ORDER BY `id` DESC LIMIT 0, 1", array(
				$wp_ttw_dialogues_table_name,
				0,
				$user_guid
			) ),
			ARRAY_A
		);

		if (
			is_null( $current_user_dialogue_from_wpdb ) || 
			0 === count( $current_user_dialogue_from_wpdb )
		) {
			return VAPPV_TTW_Core::ajax_pack_error(
				'USER_NO_THREADS_YET',
				esc_html(
					/* translators: plugin exception = no dialogues (threads) matching this user were found */
					__( 'No dialogues matching this user were found.', 'talk-to-website' )
				)
			);
		}

		//Get messages from database
		$user_messages_from_wpdb = array();

		//Prepare SQL request and get rows
		$list_messages_sql_request = "";
		if (
			is_null( $last_known_id ) || 
			'' === $last_known_id ||
			-1 === $last_known_id
		) {
			//Direct listing without last known identifier
			//Usually, the first listing after entering "Testing" tab
			if ( 'backward' === $data_direction ) {

				//List last N messages
				$user_messages_from_wpdb = $wpdb->get_results(
					$wpdb->prepare( "SELECT * FROM %i WHERE `related_user_guid`=%s AND (`message_type`=%s OR `message_type`=%s) ORDER BY `id` DESC LIMIT 0, %d", array(
						$wp_ttw_dialogues_messages_table_name,
						$user_guid,
						'message_by_visitor',
						'response_for_visitor',
						$data_limit
					) ),
					ARRAY_A
				);

			} elseif ( 'forward' === $data_direction ) {

				//Error. Cannot list messages from the future
				return VAPPV_TTW_Core::ajax_pack_error(
					'LIST_MESSAGES_WRONG_PARAM',
					esc_html(
						/* translators: plugin exception = cannot list messages with last_known_id=null and date_direction=forward */
						__( 'Wrong combination of params. You cannot list messages with last_known_id=null and data_direction=forward.', 'talk-to-website' )
					)
				);

			}
		} else {
			//List messages from specified ID to some direction
			if ( 'backward' === $data_direction ) {

				//List last N messages from the specified ID
				//Scrolling messages from current position to the top (listing old messages)
				$user_messages_from_wpdb = $wpdb->get_results(
					$wpdb->prepare( "SELECT * FROM %i WHERE `related_user_guid`=%s AND (`message_type`=%s OR `message_type`=%s) AND `id`<%d ORDER BY `id` DESC LIMIT 0, %d", array(
						$wp_ttw_dialogues_messages_table_name,
						$user_guid,
						'message_by_visitor',
						'response_for_visitor',
						$last_known_id,
						$data_limit
					) ),
					ARRAY_A
				);

			} elseif ( 'forward' === $data_direction ) {

				//List new N messages from the specified ID
				//Trying to fetch new messages after specified.
				$user_messages_from_wpdb = $wpdb->get_results(
					$wpdb->prepare( "SELECT * FROM %i WHERE `related_user_guid`=%s AND (`message_type`=%s OR `message_type`=%s) AND `id`>%d ORDER BY `id` ASC LIMIT 0, %d", array(
						$wp_ttw_dialogues_messages_table_name,
						$user_guid,
						'message_by_visitor',
						'response_for_visitor',
						$last_known_id,
						$data_limit
					) ),
					ARRAY_A
				);

			}
		}

		//Check are more messages in specified direction
		$are_more_messages_in_specified_direction = false;
		$min_id_in_selection = 100000000;
		$max_id_in_selection = -1;

		for ( $q = 0; $q < count( $user_messages_from_wpdb ); $q++ ) {
			if ( $user_messages_from_wpdb[ $q ]['id'] < $min_id_in_selection ) {
				$min_id_in_selection = $user_messages_from_wpdb[ $q ]['id'];
			}
			if ( $user_messages_from_wpdb[ $q ]['id'] > $max_id_in_selection ) {
				$max_id_in_selection = $user_messages_from_wpdb[ $q ]['id'];
			}
		}

		if ( 'backward' === $data_direction ) {

			$check_more_messages_in_specified_direction_rows = $wpdb->get_results(
				$wpdb->prepare( "SELECT COUNT(*) AS `next_items_count` FROM %i WHERE `related_user_guid`=%s AND (`message_type`=%s OR `message_type`=%s) AND `id` < %d", array(
					$wp_ttw_dialogues_messages_table_name,
					$user_guid,
					'message_by_admin',
					'response_for_admin',
					$min_id_in_selection
				) ),
				ARRAY_A
			);

		} elseif ( 'forward' === $data_direction ) {

			$check_more_messages_in_specified_direction_rows  = $wpdb->get_results(
				$wpdb->prepare( "SELECT COUNT(*) AS `next_items_count` FROM %i WHERE `related_user_guid`=%s AND (`message_type`=%s OR `message_type`=%s) AND `id` > %d", array(
					$wp_ttw_dialogues_messages_table_name,
					$user_guid,
					'message_by_admin',
					'response_for_admin',
					$max_id_in_selection
				) ),
				ARRAY_A
			);

		}

		if ( $check_more_messages_in_specified_direction_rows[0]['next_items_count'] > 0 ) {
			$are_more_messages_in_specified_direction = true;
		}

		return VAPPV_TTW_Core::ajax_pack_success( array(
			'Messages'        => $user_messages_from_wpdb,
			'AreMoreMessages' => $are_more_messages_in_specified_direction
		) );

	}

	/**
	 * Set URLs followed into specified message
	 *
	 * @param $message_id integer Message numeric identifier
	 * @param $message_guid string Message GUID (text identifier)
	 * @param $followed_url string Followed URL text
	 *
	 * @return array
	 * @throws Exception
	 */
	public function set_urls_followed_into_message(
		int $message_id,
		string $message_guid,
		string $followed_url
	): array {

		global $wpdb;

		//Prepare args length
		if ( mb_strlen( $message_guid, 'utf-8' ) > 40 ) {
			$message_guid = mb_substr( $message_guid, 0, 40, 'utf-8' );
		}
		if ( mb_strlen( $followed_url, 'utf-8' ) > 1000 ) {
			$followed_url = mb_substr( $followed_url, 0, 1000, 'utf-8' );
		}

		//Extract data from the local DB
		$db_tables_prefix = $wpdb->prefix;
		$wp_ttw_dialogues_messages_table_name = "{$db_tables_prefix}vappv_ttw_dialogues_messages";

		//Get targeted message
		$read_message_rows_from_wpdb = $wpdb->get_results(
			$wpdb->prepare( "SELECT * FROM %i WHERE `id`=%d AND `message_guid`=%s", array(
				$wp_ttw_dialogues_messages_table_name,
				$message_id,
				$message_guid
			) ),
			ARRAY_A
		);

		//Update params for specified message
		if (
			! is_null( $read_message_rows_from_wpdb ) && 
			count( $read_message_rows_from_wpdb ) > 0
		) {
			$message_data = $read_message_rows_from_wpdb[0];
			$message_read_metadata = $message_data['read_by_user_metadata'];
			if (
				is_null( $message_read_metadata ) || 
				'' === $message_read_metadata
			) {
				$message_read_metadata = array(
					'followed_urls_history' => array()
				);
			} else {
				$message_read_metadata = json_decode( $message_read_metadata, true );
			}

			//Add new followed url
			array_push( $message_read_metadata['followed_urls_history'], array(
				'date' => gmdate( 'Y-m-d H:i:s' ),
				'url'  => $followed_url
			) );

			//Get stringified array data
			$message_read_metadata_stringifyed = wp_json_encode( $message_read_metadata );

			//Update message meta
			$wpdb->update(
				$wp_ttw_dialogues_messages_table_name,
				array(
					'is_url_followed_by_sender' => 1,
					'read_by_user_metadata'     => $message_read_metadata_stringifyed
				),
				array(
					'id'           => $message_id,
					'message_guid' => $message_guid
				)
			);

			return VAPPV_TTW_Core::ajax_pack_success( array( "OperationResults" =>
				esc_html(
					/* translators: url into message marked as followed */
					__( 'URL into message marked as followed.', 'talk-to-website' )
				)
			) );
		}

		//Message not found
		return VAPPV_TTW_Core::ajax_pack_error(
			'MESSAGE_NOT_FOUND',
			esc_html(
				/* translators: plugin exception = cannot get message with specified id / guid */
				__( 'Cannot get message with specified id/guid.', 'talk-to-website' )
			)
		);

	}

	/* --------------------------------------------------------------- */
	/*                        Admin functions                          */
	/* --------------------------------------------------------------- */

	/**
	 * Send message to AI as administrator
	 *
	 * @param string $message_guid Message GUID
	 * @param string $message_text Message text
	 *
	 * @return array
	 * @throws Exception
	 */
	public function send_message_to_ai_as_admin(
		string $message_guid,
		string $message_text
	): array {

		global $wpdb;

		//Create controllers
		$settings_controller = new VAPPV_TTW_Settings();
		$settings_controller->load_all_saved_settings();

		//------------------------------------------------
		//Check daily limits
		$ttw_stats_controller = new VAPPV_TTW_Statistics();
		$can_we_process_message_based_on_daily_limits = $ttw_stats_controller->can_process_message_from_user( $settings_controller, $message_text );
		if ( false === $can_we_process_message_based_on_daily_limits ) {
			return VAPPV_TTW_Core::ajax_pack_error(
				'DAILY_CONVERSATION_LIMIT_EXCEEDED',
				esc_html(
					/* translators: plugin exception = daily limit of communication with the AI has been exceeded */
					__( 'The daily limit of communication with the AI has been exceeded, based on the plugin settings. Check the [Settings] tab, [Input / output limits] section.', 'talk-to-website' )
				)
			);
		}
		//------------------------------------------------

		//Create ChatGPT controller
		$openai_api_address = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_openai_api_address );
		$openai_api_key = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_openai_api_key );
		$ttw_openai_api_controller = new VAPPV_TTW_OpenAI_API_Connector( $openai_api_address, $openai_api_key );

		//Get current assistant data from DB
		$chatgpt_current_assistant_data_raw = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_current_assistant_data );
		if ( is_null( $chatgpt_current_assistant_data_raw ) ) {
			//No assistant associated. Throw an exception.
			return VAPPV_TTW_Core::ajax_pack_error(
				'CHATGPT_ASSISTANT_NOT_CREATED_YET',
				esc_html(
					/* translators: plugin exception = failed to find assistant data, please re-train the plugin */
					__( 'Failed to find assistant data. Please re-train the plugin.', 'talk-to-website' )
				)
			);
		}
		$chatgpt_current_assistant_data_object = json_decode( $chatgpt_current_assistant_data_raw, true );
		$chatgpt_assistant_id = $chatgpt_current_assistant_data_object['id'];

		//Extract data from the local DB
		$db_tables_prefix = $wpdb->prefix;
		$wp_ttw_dialogues_table_name = "{$db_tables_prefix}vappv_ttw_dialogues";
		$wp_ttw_dialogues_messages_table_name = "{$db_tables_prefix}vappv_ttw_dialogues_messages";
		$settings_dialogue_lifetime_minutes = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_dialogue_lifetime_minutes );
		$settings_dialogue_lifetime_max_messages = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_dialogue_lifetime_max_messages );
		$current_date_time = new DateTime( gmdate( 'Y-m-d H:i:s' ) );
		$max_old_date_time_for_thread = null;

		if ( -1 !== $settings_dialogue_lifetime_minutes ) {
			$max_old_date_time_for_thread = new DateTime( gmdate( 'Y-m-d H:i:s' ) );
			$max_old_date_time_for_thread->sub( new DateInterval( "PT{$settings_dialogue_lifetime_minutes}M" ) );
		}

		//#1 Extract dialogue information or create a new dialogue
		$dialogue_data = null;
		$current_admin_dialogue_from_wpdb = array();
		if ( -1 !== $settings_dialogue_lifetime_minutes ) {
			$current_admin_dialogue_from_wpdb = $wpdb->get_results(
				$wpdb->prepare( "SELECT * FROM %i WHERE `is_thread_by_admin`=%d AND `start_date`>=%s ORDER BY `id` DESC LIMIT 0, 1", array(
					$wp_ttw_dialogues_table_name,
					1,
					$max_old_date_time_for_thread->format( 'Y-m-d H:i:s' )
				) ),
				ARRAY_A
			);
		} else {
			$current_admin_dialogue_from_wpdb = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i WHERE `is_thread_by_admin`=%d ORDER BY `id` DESC LIMIT 0, 1", array( $wp_ttw_dialogues_table_name, 1 ) ), ARRAY_A );
		}

		//Check extracted thread object
		$should_restart_thread_by_messages_count_limit = false;
		$extracted_thread_assistant_id = null;
		if (
			! is_null( $current_admin_dialogue_from_wpdb ) &&
			count( $current_admin_dialogue_from_wpdb ) > 0
		) {
			//Get thread assistant id
			$extracted_thread_assistant_id = $current_admin_dialogue_from_wpdb[0]['ai_assistant_id'];

			//---------------------------------
			//Check thread messages limit

			//Get thread current messages count
			if ( -1 !== $settings_dialogue_lifetime_max_messages ) {
				$current_dialogue_messages_count_from_wpdb_rows = $wpdb->get_results( $wpdb->prepare( "SELECT COUNT(*) AS `thread_messages_count` FROM %i WHERE `dialogue_id`=%d", array( $wp_ttw_dialogues_messages_table_name, $current_admin_dialogue_from_wpdb[0]['id'] ) ), ARRAY_A );
				if ( $current_dialogue_messages_count_from_wpdb_rows != null && count( $current_dialogue_messages_count_from_wpdb_rows ) > 0 ) {
					$dialogue_messages_count = $current_dialogue_messages_count_from_wpdb_rows[0]['thread_messages_count'];

					//Compare messages count and limit
					if ( $dialogue_messages_count > $settings_dialogue_lifetime_max_messages ) {
						$should_restart_thread_by_messages_count_limit = true;
					}
				}
			}
			//---------------------------------
		}

		//Decide what to do: create a new thread or use existing one
		$is_new_dialogue_started = false;
		if (
			is_null( $current_admin_dialogue_from_wpdb ) ||
			0 === count( $current_admin_dialogue_from_wpdb ) ||
			$extracted_thread_assistant_id !== $chatgpt_assistant_id ||
			true === $should_restart_thread_by_messages_count_limit
		) {
			//Create new thread into ChatGPT and extract its data
			$chatgpt_create_thread_results = $ttw_openai_api_controller->create_thread();
			if ( ! is_array( $chatgpt_create_thread_results ) ) {
				if (
					'VAPPV_TTW_OpenAI_API_Error' === get_class( $chatgpt_create_thread_results ) ||
					'VAPPV_TTW_Common_Error' === get_class( $chatgpt_create_thread_results )
				) {
					return VAPPV_TTW_Core::ajax_pack_error(
						'CHATGPT_CREATE_THREAD_ERROR',
						sprintf(
							'%1$s // %2$s',
							esc_html(
								/* translators: plugin exception = failed to create ChatGPT thread via API */
								__( 'Failed to create ChatGPT thread.', 'talk-to-website' )
							),
							esc_html( $chatgpt_create_thread_results->message )
						)
					);
				}
			}

			//Save data into DB
			$dialogue_data = array(
				'start_date'           => gmdate( 'Y-m-d H:i:s' ),
				'user_name'            => 'Admin',
				'user_email'           => null,
				'user_guid'            => null,
				'user_ip'              => null,
				'is_thread_by_admin'   => 1,
				'ai_thread_id'         => $chatgpt_create_thread_results['id'],
				'ai_assistant_id'      => $chatgpt_assistant_id,
				'ai_thread_all_params' => wp_json_encode( $chatgpt_create_thread_results )
			);
			$wpdb->insert(
				$wp_ttw_dialogues_table_name,
				$dialogue_data
			);
			$lastInsertedEntryId = $wpdb->insert_id;
			$dialogue_data['id'] = $lastInsertedEntryId;
			$is_new_dialogue_started = true;

		} else {
			//Extract existing dialogue data
			$dialogue_data = $current_admin_dialogue_from_wpdb[0];
		}

		//Deal with the dialogue
		//#2 Create message to the thread
		$chatgpt_create_new_message_results = $ttw_openai_api_controller->create_message( $dialogue_data["ai_thread_id"], $message_text );
		if ( ! is_array( $chatgpt_create_new_message_results ) ) {
			if (
				'VAPPV_TTW_OpenAI_API_Error' === get_class( $chatgpt_create_new_message_results ) ||
				'VAPPV_TTW_Common_Error' === get_class( $chatgpt_create_new_message_results )
			) {
				return VAPPV_TTW_Core::ajax_pack_error(
					'CHATGPT_CREATE_MESSAGE_ERROR',
					sprintf(
						'%1$s // %2$s',
						esc_html(
							/* translators: plugin exception = failed to create new message into chat with AI via API */
							__( 'Failed to create ChatGPT message.', 'talk-to-website' )
						),
						esc_html( $chatgpt_create_new_message_results->message )
					)
				);
			}
		}

		//Get settings for specified run
		$chatgpt_current_temperature = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_chatgpt_temperature );

		//#3 Create and launch the run
		$chatgpt_create_new_run_results = $ttw_openai_api_controller->create_run( $dialogue_data['ai_thread_id'], $chatgpt_assistant_id, $chatgpt_current_temperature, $settings_dialogue_lifetime_max_messages, true );
		if ( ! is_array( $chatgpt_create_new_run_results ) ) {
			if (
				'VAPPV_TTW_OpenAI_API_Error' === get_class( $chatgpt_create_new_run_results ) ||
				'VAPPV_TTW_Common_Error' === get_class( $chatgpt_create_new_run_results )
			) {
				return VAPPV_TTW_Core::ajax_pack_error(
					'CHATGPT_CREATE_RUN_ERROR',
					sprintf(
						'%1$s // %2$s',
						esc_html(
							/* translators: plugin exception = failed to create ChatGPT run via API */
							__( 'Failed to create ChatGPT run.', 'talk-to-website' )
						),
						esc_html( $chatgpt_create_new_run_results->message )
					)
				);
			}
		}

		//#4 Save all necessary data into DB and return packed object
		//Save data into DB
		$message_data = array(
			'dialogue_id'               => $dialogue_data['id'],
			'sender_role'               => 'user',
			'message_type'              => 'message_by_admin',
			'message_guid'              => $message_guid,
			'message_text'              => $message_text,
			'message_date'              => gmdate( 'Y-m-d H:i:s' ),
			'response_date'             => null,
			'ai_message_id'             => $chatgpt_create_new_message_results['id'],
			'ai_thread_id'              => $dialogue_data['ai_thread_id'],
			'ai_run_id'                 => $chatgpt_create_new_run_results['id'],
			'is_responded_by_ai'        => null,
			'is_executed_with_error'    => null,
			'is_read_by_sender'         => null,
			'rate_by_sender'            => null,
			'is_url_followed_by_sender' => null
		);
		$wpdb->insert(
			$wp_ttw_dialogues_messages_table_name,
			$message_data
		);
		$lastInsertedEntryId = $wpdb->insert_id;
		$message_data['id'] = $lastInsertedEntryId;

		//-----------------------------------------
		//Add stats data
		$dialogues_count = ( $is_new_dialogue_started == true ? 1 : 0 );
		$income_words_count = count( explode( ' ', $message_text ) );
		$income_characters_count = mb_strlen( $message_text, 'utf-8' );
		$ttw_stats_controller = new VAPPV_TTW_Statistics();
		$ttw_stats_controller->add_stats( $dialogues_count, 1, $income_words_count, $income_characters_count, 0, 0, 0 );
		//-----------------------------------------

		//Try to get current conversation thread
		return VAPPV_TTW_Core::ajax_pack_success( array(
			'MessageResponse' => esc_html(
				/* translators: message has been sent to the AI */
				__( 'Message has been sent, waiting for a response from AI. Please check the results after some time.', 'talk-to-website' )
			),
			'InsertedMessage' => $message_data
		) );

	}

	/**
	 * Try extract awaiting responses from AI as admin. Get results as JSON.
	 * @return array
	 * @throws Exception
	 */
	public function try_extract_response_from_ai_as_admin(): array {

		global $wpdb;

		//Create controllers
		$settings_controller = new VAPPV_TTW_Settings();
		$settings_controller->load_all_saved_settings();

		//Create ChatGPT controller
		$openai_api_address = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_openai_api_address );
		$openai_api_key = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_openai_api_key );
		$ttw_openai_api_controller = new VAPPV_TTW_OpenAI_API_Connector( $openai_api_address, $openai_api_key );

		//Get current assistant data from DB
		$chatgpt_current_assistant_data_raw = $settings_controller->try_get_setting( VAPPV_TTW_Settings::$settings_current_assistant_data );
		if ( is_null( $chatgpt_current_assistant_data_raw ) ) {
			//No assistant associated. Throw an exception.
			return VAPPV_TTW_Core::ajax_pack_error(
				'CHATGPT_ASSISTANT_NOT_CREATED_YET',
				esc_html(
					/* translators: plugin exception = failed to find assistant data, please re-train the plugin */
					__( 'Failed to find assistant data. Please re-train the plugin.', 'talk-to-website' )
				)
			);
		}
		$chatgpt_current_assistant_data_object = json_decode( $chatgpt_current_assistant_data_raw, true );
		$chatgpt_assistant_id = $chatgpt_current_assistant_data_object['id'];

		//Extract data from the local DB
		$db_tables_prefix = $wpdb->prefix;
		$wp_ttw_dialogues_table_name = "{$db_tables_prefix}vappv_ttw_dialogues";
		$wp_ttw_dialogues_messages_table_name = "{$db_tables_prefix}vappv_ttw_dialogues_messages";

		//Try to get unanswered messages from admin
		$admin_pending_messages_from_wpdb = $wpdb->get_results(
			$wpdb->prepare( "SELECT * FROM %i WHERE `sender_role`=%s AND `message_type`=%s AND `is_responded_by_ai` IS NULL AND `is_executed_with_error` IS NULL ORDER BY `id` DESC LIMIT 0, 1", array(
				$wp_ttw_dialogues_messages_table_name,
				'user',
				'message_by_admin'
			) ),
			ARRAY_A
		);

		if (
			is_null( $admin_pending_messages_from_wpdb ) || 
			0 === count( $admin_pending_messages_from_wpdb )
		) {
			return VAPPV_TTW_Core::ajax_pack_success( array(
				'ExtractionResults' => 'no_new_messages',
				'Response'          => esc_html(
					/* translators: no queued / unanswered messages found */
					__( 'No queued (unanswered) messages found.', 'talk-to-website' )
				)
			) );
		}

		//Get unanswered message
		$pending_message_data = $admin_pending_messages_from_wpdb[0];

		//#1 Try to resolve ChatGPT run
		$chatgpt_retrieve_run_results = $ttw_openai_api_controller->retrieve_run( $pending_message_data['ai_thread_id'], $pending_message_data['ai_run_id'] );
		if ( ! is_array( $chatgpt_retrieve_run_results ) ) {
			if (
				'VAPPV_TTW_OpenAI_API_Error' === get_class( $chatgpt_retrieve_run_results ) ||
				'VAPPV_TTW_Common_Error' === get_class( $chatgpt_retrieve_run_results )
			) {
				return VAPPV_TTW_Core::ajax_pack_error(
					'CHATGPT_RETRIEVE_RUN_ERROR',
					sprintf(
						'%1$s // %2$s',
						esc_html(
							/* translators: plugin exception = failed to retrieve run from ChatGPT */
							__( 'Failed to retrieve run from ChatGPT.', 'talk-to-website' )
						),
						esc_html( $chatgpt_retrieve_run_results->message )
					)
				);
			}
		}

		//Analyse run results
		//Check for failed runs
		if (
			'failed'            === $chatgpt_retrieve_run_results['status'] ||
			'cancelled'         === $chatgpt_retrieve_run_results['status'] ||
			'cancelling'        === $chatgpt_retrieve_run_results['status'] ||
			'expired'           === $chatgpt_retrieve_run_results['status'] ||
			'required_action'   === $chatgpt_retrieve_run_results['status']
		) {
			//Failed to answer user message. Execute response as failed.
			$occured_error_details = $chatgpt_retrieve_run_results['last_error'];
			$wpdb->update(
				$wp_ttw_dialogues_messages_table_name,
				array(
					'is_executed_with_error' => 1,
					'ai_error_code'          => esc_html( "{$chatgpt_retrieve_run_results['status']}/{$occured_error_details['code']}" ),
					'ai_error_details'       => esc_html( $occured_error_details['message'] ),
					'response_date'          => gmdate( 'Y-m-d H:i:s' )
				),
				array(
					'id' => $pending_message_data['id']
				)
			);

			//Return results and stop execution
			return VAPPV_TTW_Core::ajax_pack_success( array(
				'ExtractionResults' => 'last_message_executed_with_error',
				'ErrorCode'         => $occured_error_details['code'],
				'ErrorDetails'      => $occured_error_details['message'],
				'MessageId'         => $pending_message_data->id,
				'Response'          => esc_html(
					/* translators: plugin exception = ChatGPT thrown an error during message execution */
					__( 'ChatGPT thrown an error during message execution.', 'talk-to-website' )
				)
			) );
		}

		//Check for processing / queued runs
		if (
			'queued'        === $chatgpt_retrieve_run_results['status'] ||
			'in_progress'   === $chatgpt_retrieve_run_results['status']
		) {
			//Anwer is not ready yet
			return VAPPV_TTW_Core::ajax_pack_success( array(
				'ExtractionResults' => 'ai_answer_is_still_pending',
				'Response'          =>
					sprintf(
						'%1$s // %2$s',
						esc_html(
							/* translators: the response from the AI is not yet ready */
							__( 'The response from the AI is not yet ready.', 'talk-to-website' )
						),
						esc_html( "(status = {$chatgpt_retrieve_run_results['status']})" )
					)
			) );
		}

		//Prepare usage stats containers
		$usageStats = null;
		$ai_execution_stats_json = null;

		//Check for completed runs
		if ( 'completed' === $chatgpt_retrieve_run_results['status'] ) {

			//Run is completed successfully
			$usageStats = $chatgpt_retrieve_run_results['usage'];
			$ai_execution_stats_json = wp_json_encode( $usageStats );

			$wpdb->update(
				$wp_ttw_dialogues_messages_table_name,
				array(
					'is_responded_by_ai' => 1,
					'response_date'      => gmdate( 'Y-m-d H:i:s' )
				),
				array( 'id' => $pending_message_data['id'] )
			);
		}

		//#2 Fetch new messages from ChatGPT if success
		$chatgpt_get_messages_results = $ttw_openai_api_controller->list_messages( $pending_message_data['ai_thread_id'], $pending_message_data['ai_message_id'], 20 );
		if ( ! is_array( $chatgpt_get_messages_results ) ) {
			if (
				'VAPPV_TTW_OpenAI_API_Error' === get_class( $chatgpt_get_messages_results ) ||
				'VAPPV_TTW_Common_Error' === get_class( $chatgpt_get_messages_results )
			) {
				return VAPPV_TTW_Core::ajax_pack_error(
					'CHATGPT_RETRIEVE_RUN_ERROR',
					sprintf(
						'%1$s // %2$s',
						esc_html(
							/* translators: plugin exception = failed to retrieve run from ChatGPT */
							__( 'Failed to retrieve run from ChatGPT.', 'talk-to-website' )
						),
						esc_html( $chatgpt_get_messages_results->message )
					)
				);
			}
		}

		//Add listed messages to the DB (test purpose)
		//$settings_controller->update_setting( VAPPV_TTW_Settings::$settings_last_openai_answer, wp_json_encode( $chatgpt_get_messages_results ) );

		//Prepare containers
		$messages_data = $chatgpt_get_messages_results['data'];
		$messages_packed_results_ttw = array();

		//Prepare counters
		$output_words_count_total = 0;
		$output_characters_count_total = 0;
		$output_tokens_count_total = 0;

		//Increment completion tokens count
		if (
			! is_null( $usageStats ) && 
			isset( $usageStats['completion_tokens'] )
		) {
			$output_tokens_count_total += $usageStats['completion_tokens'];
		}

		//Itterate answers (maybe many messages)
		for ( $aim = 0; $aim < count( $messages_data ); $aim++ ) {
			//Skip messages from user (copy of already stored messages)
			if ( isset( $messages_data[ $aim ]['metadata'] ) ) {
				if ( isset( $messages_data[ $aim ]['metadata']['is_message_from_user'] ) ) {
					if (
						'1' === $messages_data[ $aim ]['metadata']['is_message_from_user'] ||
						1 === (int)$messages_data[ $aim ]['metadata']['is_message_from_user']
					) {
						//This is a copy of user message. Just keep working.
						continue;
					}
				}
			}

			//Operated answers from AI
			$next_message_response_full_text = '';
			$message_content = $messages_data[ $aim ]['content'];
			for ( $mc = 0; $mc < count( $message_content ); $mc++ ) {
				if ( 'text' === $message_content[ $mc ]['type'] ) {
					//Get text part of the answer
					$next_content_part = $message_content[ $mc ]['text']['value'];

					//Check for citation (annotations and quotes from file search)
					if ( isset( $message_content[ $mc ]['text']['annotations'] ) ) {
						if ( is_array( $message_content[ $mc ]['text']['annotations'] ) ) {
							for ( $ac = 0; $ac < count( $message_content[ $mc ]['text']['annotations'] ); $ac++ ) {
								$next_annotation = $message_content[ $mc ]['text']['annotations'][ $ac ];
								if ( 'file_citation' === $next_annotation['type'] ) {
									$text_to_be_replaced = $next_annotation['text'];
									$replacement = '';
									//$replacement         = $next_annotation['file_citation']['quote'];
									if ( false !== strpos( $next_content_part, $text_to_be_replaced ) ) {
										$next_content_part = str_replace( $text_to_be_replaced, $replacement, $next_content_part );
									}
								}
							}
						}
					}

					//Add content part to the whole ansert
					if ( 0 === $mc ) {
						$next_message_response_full_text .= $next_content_part;
					} else {
						$next_message_response_full_text .= ( '; ' . $next_content_part );
					}
				}
			}

			//Increment stats
			if ( '' !== $next_message_response_full_text ) {
				$output_words_count_total += count( explode( ' ', $next_message_response_full_text ) );
				$output_characters_count_total += mb_strlen( $next_message_response_full_text, 'utf-8' );
			}

			//Insert new message to DB
			//Save data into DB
			$message_data = array(
				'dialogue_id'               => $pending_message_data['dialogue_id'],
				'sender_role'               => 'ai',
				'message_type'              => 'response_for_admin',
				'message_guid'              => VAPPV_TTW_Core::create_guid(),
				'message_text'              => $next_message_response_full_text,
				'message_date'              => gmdate( 'Y-m-d H:i:s' ),
				'response_date'             => null,
				'ai_message_id'             => $messages_data[ $aim ]['id'],
				'ai_thread_id'              => $messages_data [ $aim ]['thread_id'],
				'ai_run_id'                 => $messages_data[ $aim ]['run_id'],
				'is_responded_by_ai'        => null,
				'is_executed_with_error'    => null,
				'is_read_by_sender'         => null,
				'rate_by_sender'            => null,
				'is_url_followed_by_sender' => null,
				'ai_execution_stats'        => $ai_execution_stats_json
			);
			$wpdb->insert(
				$wp_ttw_dialogues_messages_table_name,
				$message_data
			);
			$lastInsertedEntryId = $wpdb->insert_id;
			$message_data['id'] = $lastInsertedEntryId;

			array_push( $messages_packed_results_ttw, $message_data );
		}

		//-----------------------------------------
		//Add stats data
		$ttw_stats_controller = new VAPPV_TTW_Statistics();
		$ttw_stats_controller->add_stats( 0, 1, 0, 0, $output_words_count_total, $output_characters_count_total, $output_tokens_count_total );

		//-----------------------------------------

		return VAPPV_TTW_Core::ajax_pack_success( array(
			'ExtractionResults' => 'messages_are_extracted',
			'Response'          => esc_html(
				/* translators: responses from ChatGPT extracted and recorded in the database */
				__( 'Responses from ChatGPT extracted and recorded in the database.', 'talk-to-website' )
			),
			'Messages'          => $messages_packed_results_ttw
		) );

	}

	/**
	 * List testing thread messages as admin. Get results as JSON.
	 *
	 * @param string $data_direction Direction of messages extraction { "backward" || "forward" }
	 * @param int $last_known_id ID of the last known message
	 * @param int $data_limit Data limit
	 *
	 * @return array
	 * @throws Exception
	 */
	public function list_testing_messages_as_admin(
		string $data_direction,
		int $last_known_id,
		int $data_limit
	): array {

		global $wpdb;

		//Create controllers
		$settings_controller = new VAPPV_TTW_Settings();
		$settings_controller->load_all_saved_settings();

		//Extract data from the local DB
		$db_tables_prefix = $wpdb->prefix;
		$wp_ttw_dialogues_table_name = "{$db_tables_prefix}vappv_ttw_dialogues";
		$wp_ttw_dialogues_messages_table_name = "{$db_tables_prefix}vappv_ttw_dialogues_messages";

		//Get messages from database
		$admin_messages_from_wpdb = array();

		//Prepare SQL request
		if (
			is_null( $last_known_id ) ||
			'' === $last_known_id ||
			-1 === $last_known_id
		) {
			//Direct listing without last known identifier
			//Usually, the first listing after entering "Testing" tab
			if ( 'backward' === $data_direction ) {

				//Liast last N messages
				$admin_messages_from_wpdb = $wpdb->get_results(
					$wpdb->prepare( "SELECT * FROM %i WHERE (`message_type`=%s OR `message_type`=%s) ORDER BY `id` DESC LIMIT 0, %d", array(
						$wp_ttw_dialogues_messages_table_name,
						'message_by_admin',
						'response_for_admin',
						$data_limit
					) ),
					ARRAY_A
				);

			} elseif ( 'forward' === $data_direction ) {
				//Error. Cannot list messages from the future
				return VAPPV_TTW_Core::ajax_pack_error(
					'LIST_MESSAGES_WRONG_PARAM',
					esc_html(
						/* translators: plugin exception = wrong combination of params, cannot list messages with last_known_id = null and data_direction = forward */
						__( 'Wrong combination of params. You cannot list messages with last_known_id=null and data_direction=forward.', 'talk-to-website' )
					)
				);
			}
		} else {

			//List messages from specified ID to some direction
			if ( 'backward' === $data_direction ) {
				//Liast last N messages from the specified ID
				//Scrolling messages from current position to the top (listing old messages)
				$admin_messages_from_wpdb = $wpdb->get_results(
					$wpdb->prepare( "SELECT * FROM %i WHERE (`message_type`=%s OR `message_type`=%s) AND `id`<%d ORDER BY `id` DESC LIMIT 0, %d", array(
						$wp_ttw_dialogues_messages_table_name,
						'message_by_admin',
						'response_for_admin',
						$last_known_id,
						$data_limit
					) ),
					ARRAY_A
				);

			} elseif ( 'forward' === $data_direction ) {

				//List new N messages from the specified ID
				//Trying to fetch new messages after specified.
				$admin_messages_from_wpdb = $wpdb->get_results(
						$wpdb->prepare( "SELECT * FROM %i WHERE (`message_type`=%s OR `message_type`=%s) AND `id`>%d ORDER BY `id` ASC LIMIT 0, %d", array(
							$wp_ttw_dialogues_messages_table_name,
							'message_by_admin',
							'response_for_admin',
							$last_known_id,
							$data_limit
						) ),
						ARRAY_A
					);

			}
		}

		//Check are more messages in specified direction
		$are_more_messages_in_specified_direction = false;
		$min_id_in_selection = 100000000;
		$max_id_in_selection = -1;
		for ( $q = 0; $q < count( $admin_messages_from_wpdb ); $q++ ) {
			if ( $admin_messages_from_wpdb[ $q ]['id'] < $min_id_in_selection ) {
				$min_id_in_selection = $admin_messages_from_wpdb[ $q ]['id'];
			}
			if ( $admin_messages_from_wpdb[ $q ]['id'] > $max_id_in_selection ) {
				$max_id_in_selection = $admin_messages_from_wpdb[ $q ]['id'];
			}
		}
		if ( 'backward' === $data_direction ) {

			$check_more_messages_in_specified_direction_rows = $wpdb->get_results(
				$wpdb->prepare( "SELECT COUNT(*) AS `next_items_count` FROM %i WHERE (`message_type`=%s OR `message_type`=%s) AND `id` < %d", array(
					$wp_ttw_dialogues_messages_table_name,
					'message_by_admin',
					'response_for_admin',
					$min_id_in_selection
				) ),
				ARRAY_A
			);

		} elseif ( 'forward' === $data_direction ) {

			$check_more_messages_in_specified_direction_rows = $wpdb->get_results(
				$wpdb->prepare( "SELECT COUNT(*) AS `next_items_count` FROM %i WHERE (`message_type`=%s OR `message_type`=%s) AND `id` > %d", array(
					$wp_ttw_dialogues_messages_table_name,
					'message_by_admin',
					'response_for_admin',
					$max_id_in_selection
				) ),
				ARRAY_A
			);

		}
		if ( $check_more_messages_in_specified_direction_rows[0]['next_items_count'] > 0 ) {
			$are_more_messages_in_specified_direction = true;
		}

		return VAPPV_TTW_Core::ajax_pack_success( array(
			'Messages'        => $admin_messages_from_wpdb,
			'AreMoreMessages' => $are_more_messages_in_specified_direction
		) );

	}
}

/*
	© 2024 Copyright Vitalii A. Popov. All rights reserved.
*/

?>