<?php
/**
 * This file contains all the API requests for the plugin.
 *
 * @package Webdigit
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use Orhanerday\OpenAi\OpenAi;

add_action(
	'rest_api_init',
	function () {
		register_rest_route(
			'wdgpt/v1',
			'toggle-summary',
			array(
				'methods'             => 'POST',
				'callback'            => 'wdgpt_toggle_summary',
				'permission_callback' => '__return_true',
			)
		);
	}
);

add_action(
	'rest_api_init',
	function () {
		register_rest_route(
			'wdgpt/v1',
			'purge-error-logs',
			array(
				'methods'             => 'POST',
				'callback'            => 'wdgpt_purge_error_logs',
				'permission_callback' => 'wdgpt_is_authorised_to_do',
			)
		);
	}
);

add_action(
	'rest_api_init',
	function () {
		register_rest_route(
			'wdgpt/v1',
			'purge-chat-logs',
			array(
				'methods'             => 'POST',
				'callback'            => 'wdgpt_purge_chat_logs',
				'permission_callback' => 'wdgpt_is_authorised_to_do',
			)
		);
	}
);

add_action(
	'rest_api_init',
	function () {
		register_rest_route(
			'wdgpt/v1',
			'export-chat-logs-txt',
			array(
				'methods'             => 'GET',
				'callback'            => 'wdgpt_export_chat_logs_txt',
				'permission_callback' => 'wdgpt_is_authorised_to_do',
			)
		);
	}
);

add_action(
	'rest_api_init',
	function () {
		register_rest_route(
			'wdgpt/v1',
			'download-conversation-txt/(?P<log_id>\d+)',
			array(
				'methods'             => 'GET',
				'callback'            => 'wdgpt_download_conversation_txt',
				'permission_callback' => 'wdgpt_is_authorised_to_do',
				'args'                => array(
					'log_id' => array(
						'required'          => true,
						'type'              => 'integer',
						'sanitize_callback' => 'absint',
					),
				),
			)
		);
	}
);

add_action(
	'rest_api_init',
	function () {
		register_rest_route(
			'wdgpt/v1',
			'retrieve-content/(?P<post_id>\d+)',
			array(
				'methods'             => 'GET',
				'callback'            => 'wdgpt_retrieve_content',
				'permission_callback' => '__return_true',
			)
		);
	}
);

add_action(
	'rest_api_init',
	function () {
		register_rest_route(
			'wdgpt/v1',
			'save-embeddings',
			array(
				'methods'             => 'POST',
				'callback'            => 'wdgpt_save_embeddings',
				'permission_callback' => '__return_true',
			)
		);
	}
);

add_action(
	'rest_api_init',
	function () {
		register_rest_route(
			'wdgpt/v1',
			'temperature',
			array(
				'methods'             => 'GET',
				'callback'            => 'wdgpt_retrieve_temperature',
				'permission_callback' => '__return_true',
			)
		);
	}
);

add_action(
	'rest_api_init',
	function () {
		register_rest_route(
			'wdgpt/v1',
			'retrieve-prompt',
			array(
				'methods'             => 'POST',
				'callback'            => 'wdgpt_retrieve_prompt',
				'permission_callback' => '__return_true',
			)
		);
	}
);

add_action(
	'rest_api_init',
	function () {
		register_rest_route(
			'wdgpt/v1',
			'update-database',
			array(
				'methods'             => 'POST',
				'callback'            => 'wdgpt_update_database',
				'permission_callback' => '__return_true',
			)
		);
	}
);

function wdgpt_is_authorised_to_do() {
	$user          = wp_get_current_user();
	$allowed_roles = array( 'administrator' );
	return array_intersect( $allowed_roles, $user->roles );
}

/**
 * Updates the database.
 *
 * @return array The result of the operation.
 */
function wdgpt_update_database() {
	try {
		$database_updater = new WDGPT_Database_Updater( wdgpt_chatbot()->get_version() );
		$database_updater->update_database();
		return array(
			'success' => true,
			'message' => __( 'Database updated successfully.', 'webdigit-chatbot' ),
		);
	} catch ( Exception $e ) {
		return array(
			'success' => false,
			'message' => $e->getMessage(),
		);
	}
}

/**
 * Accumule le texte assistant à partir d'une ligne SSE Chat Completions (data: {...}).
 *
 * cURL peut couper le flux au milieu d'une ligne : il faut assembler par lignes entières
 * avant json_decode, sinon des morceaux de delta sont perdus (symptômes visibles surtout
 * avec certains modèles à petits chunks, ex. gpt-4.1-mini).
 *
 * @param string               $payload_json   Contenu JSON après le préfixe "data: ".
 * @param string               $answer         Réponse accumulée (référence).
 * @param WDGPT_Answer_Generator $answer_generator Générateur pour les erreurs stream.
 * @return void
 */
function wdgpt_chat_completion_accumulate_sse_payload( $payload_json, &$answer, $answer_generator ) {
	$payload_json = trim( $payload_json );
	if ( '' === $payload_json || '[DONE]' === $payload_json ) {
		return;
	}
	$arr = json_decode( $payload_json, true, 512, JSON_INVALID_UTF8_SUBSTITUTE );
	if ( ! is_array( $arr ) ) {
		return;
	}
	if ( isset( $arr['error']['message'] ) ) {
		$answer_generator->wdgpt_insert_error_log_message( $arr['error']['message'], 0, 'stream_error' );
		return;
	}
	if ( isset( $arr['choices'][0]['delta']['content'] ) ) {
		$answer .= $arr['choices'][0]['delta']['content'];
	}
}

/**
 * Stream via OpenAI Responses API (for reasoning models: gpt-5, o3, o4-mini).
 * Transforme le flux en format Chat Completions pour compatibilité client.
 *
 * @param string                      $api_key           API key.
 * @param string                      $model             Model ID.
 * @param object                      $messages          Messages array (role, content).
 * @param int                         $max_tokens        Max output tokens.
 * @param WDGPT_Answer_Generator      $answer_generator  Answer generator instance.
 * @return string Accumulated answer text.
 */
function wdgpt_stream_responses_api( $api_key, $model, $messages, $max_tokens, $answer_generator ) {
	$answer   = '';
	$url      = 'https://api.openai.com/v1/responses';
	$reasoning = array( 'effort' => 'minimal' );
	if ( function_exists( 'wdgpt_get_chat_params_for_model' ) ) {
		$params = wdgpt_get_chat_params_for_model( $model );
		if ( ! empty( $params['reasoning_effort'] ) ) {
			$reasoning['effort'] = $params['reasoning_effort'];
		}
	}

	// L'API Responses attend input comme tableau (même format que messages).
	$input = json_decode( wp_json_encode( $messages ), true );
	$body = array(
		'model'             => $model,
		'input'             => $input,
		'stream'            => true,
		'store'              => false,
		'max_output_tokens' => $max_tokens,
		'reasoning'         => $reasoning,
	);
	if ( function_exists( 'wdgpt_get_chat_params_for_model' ) ) {
		$params = wdgpt_get_chat_params_for_model( $model );
		if ( ! empty( $params['verbosity'] ) ) {
			// Responses API: verbosity doit être dans text.verbosity (depuis 2025).
			$body['text'] = array( 'verbosity' => $params['verbosity'] );
		}
	}

	$headers = array(
		'Authorization: Bearer ' . $api_key,
		'Content-Type: application/json',
	);

	$buffer       = '';
	$events_seen  = array();
	$delta_count  = 0;
	$ch           = curl_init( $url );
	curl_setopt_array(
		$ch,
		array(
			CURLOPT_POST            => true,
			CURLOPT_POSTFIELDS     => wp_json_encode( $body ),
			CURLOPT_HTTPHEADER      => $headers,
			CURLOPT_TIMEOUT        => 120,
			CURLOPT_WRITEFUNCTION  => function ( $ch, $data ) use ( &$answer, &$buffer, &$events_seen, &$delta_count, $answer_generator ) {
				$buffer .= $data;
				// Réponse d'erreur HTTP (JSON brut sans format SSE).
				if ( false !== strpos( $buffer, '"error"' ) && false === strpos( $buffer, "\nevent:" ) ) {
					$arr = json_decode( $buffer, true );
					if ( is_array( $arr ) && isset( $arr['error']['message'] ) && method_exists( $answer_generator, 'wdgpt_insert_error_log_message' ) ) {
						$answer_generator->wdgpt_insert_error_log_message( $arr['error']['message'], isset( $arr['error']['code'] ) ? $arr['error']['code'] : 0, 'stream_error' );
					}
					return strlen( $data );
				}
				$lines  = explode( "\n", $buffer );
				$buffer = array_pop( $lines );
				$event  = '';
				foreach ( $lines as $line ) {
					if ( 0 === strpos( $line, 'event: ' ) ) {
						$event = trim( substr( $line, 7 ) );
						if ( ! in_array( $event, $events_seen, true ) ) {
							$events_seen[] = $event;
						}
					} elseif ( 0 === strpos( $line, 'data: ' ) ) {
						$json = substr( $line, 6 );
						if ( '[DONE]' === $json || '' === trim( $json ) ) {
							continue;
						}
						$arr = json_decode( $json, true );
						if ( ! is_array( $arr ) ) {
							continue;
						}
						if ( isset( $arr['error']['message'] ) ) {
							$answer_generator->wdgpt_insert_error_log_message( $arr['error']['message'], 0, 'stream_error' );
							continue;
						}
						$ev = ! empty( $event ) ? $event : ( isset( $arr['type'] ) ? $arr['type'] : '' );
						if ( 'error' === $ev && isset( $arr['message'] ) && method_exists( $answer_generator, 'wdgpt_insert_error_log_message' ) ) {
							$answer_generator->wdgpt_insert_error_log_message( $arr['message'], 0, 'stream_error' );
							continue;
						}
						if ( 'response.output_text.delta' === $ev && ! empty( $arr['delta'] ) ) {
							$delta       = $arr['delta'];
							$answer     .= $delta;
							++$delta_count;
							// finish_reason: null requis pour que le client (index.js) traite le contenu.
							$chunk  = wp_json_encode( array( 'choices' => array( array( 'delta' => array( 'content' => $delta ), 'finish_reason' => null ) ) ) );
							// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- SSE stream.
							echo 'data: ' . $chunk . "\n\n";
							ob_flush();
							flush();
						} elseif ( 'response.completed' === $ev || 'response.output_text.done' === $ev ) {
							$chunk = wp_json_encode( array( 'choices' => array( array( 'delta' => array(), 'finish_reason' => 'stop' ) ) ) );
							// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- SSE stream.
							echo 'data: ' . $chunk . "\n\n";
							ob_flush();
							flush();
						}
					}
				}
				return strlen( $data );
			},
		)
	);

	$ok = curl_exec( $ch );
	$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
	if ( false === $ok && method_exists( $answer_generator, 'wdgpt_insert_error_log_message' ) ) {
		$answer_generator->wdgpt_insert_error_log_message( curl_error( $ch ), curl_errno( $ch ), 'stream_error' );
	} elseif ( $http_code >= 400 && ! empty( $buffer ) && method_exists( $answer_generator, 'wdgpt_insert_error_log_message' ) ) {
		$arr = json_decode( $buffer, true );
		if ( is_array( $arr ) && isset( $arr['error']['message'] ) ) {
			$answer_generator->wdgpt_insert_error_log_message( '[HTTP ' . $http_code . '] ' . $arr['error']['message'], $http_code, 'stream_error' );
		} else {
			$answer_generator->wdgpt_insert_error_log_message( '[HTTP ' . $http_code . '] ' . substr( $buffer, 0, 500 ), $http_code, 'stream_error' );
		}
	}

	// Diagnostic Responses API : log pour débogage (surtout si réponse vide).
	if ( method_exists( $answer_generator, 'wdgpt_insert_error_log_message' ) ) {
		$events_str = implode( ', ', $events_seen );
		$raw_preview = substr( preg_replace( '/\s+/', ' ', $buffer ), 0, 800 );
		$answer_generator->wdgpt_insert_error_log_message(
			sprintf(
				'Responses API [model=%s] HTTP=%d | events=%s | delta_count=%d | answer_len=%d | raw_preview=%s',
				$model,
				$http_code,
				$events_str ?: '(none)',
				$delta_count,
				strlen( $answer ),
				$raw_preview ?: '(empty)'
			),
			0,
			'responses_api_debug'
		);
	}

	curl_close( $ch );

	// Signal de fin du stream (compatibilité client Chat Completions).
	// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- SSE stream.
	echo 'data: [DONE]' . "\n\n";
	ob_flush();
	flush();

	return $answer;
}

/**
 * Retrieves the prompt from the server.
 *
 * @param WP_REST_Request $request The REST API request object.
 * @return string|WP_Error The prompt or an error object.
 */
function wdgpt_retrieve_prompt( $request ) {

	try {
		$params              = json_decode( $request->get_body(), true );
		$question            = sanitize_text_field( $params['question'] );
		$conversation        = array_map(
			function ( $conv ) {
				return array(
					'text' => sanitize_text_field( $conv['text'] ),
					'role' => sanitize_text_field( $conv['role'] ),
					'date' => sanitize_text_field( $conv['date'] ),
				);
			},
			$params['conversation']
		);
		$unique_conversation = sanitize_text_field( $params['unique_conversation'] );
		$answer_generator    = new WDGPT_Answer_Generator( $question, $conversation );
		$answer_parameters   = $answer_generator->wdgpt_retrieve_answer_parameters();

		header( 'Content-type: text/event-stream' );
		header( 'Cache-Control: no-cache' );
		// Check if $answer_parameters is an empty array.
		if ( empty( $answer_parameters ) ) {
			echo 'event: error' . PHP_EOL;
			echo 'data: ' . esc_html( __( 'Currently, there appears to be an issue. Please try asking me again later.', 'webdigit-chatbot' ) ) . PHP_EOL;
			ob_flush();
			flush();
			return '0';
		}
		$api_key                = $answer_parameters['api_key'];
		$temperature            = $answer_parameters['temperature'];
		$messages               = json_decode( wp_json_encode( $answer_parameters['messages'], JSON_INVALID_UTF8_SUBSTITUTE ) );
		$max_tokens             = $answer_parameters['max_tokens'];
		$model_type             = $answer_parameters['model_type'];
		$top_summaries_post_ids = $answer_parameters['top_summaries_post_ids'];
		$openai                 = new OpenAi( $api_key );

		$answer = '';

		// Modèles reasoning : utiliser l'API Responses (recommandé par OpenAI pour gpt-5, o3, etc.).
		if ( function_exists( 'wdgpt_is_reasoning_model' ) && wdgpt_is_reasoning_model( $model_type ) ) {
			$answer = wdgpt_stream_responses_api(
				$api_key,
				$model_type,
				$messages,
				$max_tokens,
				$answer_generator
			);
		} else {
			// Chat Completions pour les modèles standard.
			$chat_params = array(
				'model'       => $model_type,
				'messages'    => $messages,
				'temperature' => floatval( $temperature ),
				'max_tokens'  => $max_tokens,
				'stream'      => true,
			);

			if ( function_exists( 'wdgpt_get_chat_params_for_model' ) ) {
				$model_specific_params = wdgpt_get_chat_params_for_model( $model_type );
				$chat_params           = array_merge( $chat_params, $model_specific_params );
			}

			$sse_line_buffer = '';

			$chat = json_decode(
				$openai->chat(
					$chat_params,
					function ( $ch, $data ) use ( &$answer, &$sse_line_buffer, $answer_generator ) {
						$obj = json_decode( $data, false, 512, JSON_INVALID_UTF8_SUBSTITUTE );
						if ( is_object( $obj ) && isset( $obj->error->message ) ) {
							$answer_generator->wdgpt_insert_error_log_message( $obj->error->message, 0, 'stream_error' );
							return strlen( $data );
						}

						// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- SSE stream sends raw JSON.
						echo $data;

						$sse_line_buffer .= $data;
						$lines             = explode( "\n", $sse_line_buffer );
						$sse_line_buffer   = (string) array_pop( $lines );

						foreach ( $lines as $line ) {
							$line = trim( $line );
							if ( '' === $line ) {
								continue;
							}
							if ( 0 !== strpos( $line, 'data: ' ) ) {
								continue;
							}
							$payload = substr( $line, 6 );
							wdgpt_chat_completion_accumulate_sse_payload( $payload, $answer, $answer_generator );
						}

						ob_flush();
						flush();
						return strlen( $data );
					}
				)
			);

			// Dernière ligne si le flux se termine sans \n final (rare).
			$sse_line_buffer = trim( $sse_line_buffer );
			if ( '' !== $sse_line_buffer && 0 === strpos( $sse_line_buffer, 'data: ' ) ) {
				wdgpt_chat_completion_accumulate_sse_payload( substr( $sse_line_buffer, 6 ), $answer, $answer_generator );
			}
		}

		// Diagnostic : réponse vide après stream (fréquent avec modèles reasoning si max_tokens trop bas).
		if ( '' === trim( $answer ) && method_exists( $answer_generator, 'wdgpt_insert_error_log_message' ) ) {
			$answer_generator->wdgpt_insert_error_log_message(
				sprintf(
					/* translators: %s: model ID */
					__( 'Stream completed but no content received for model: %s. Consider increasing max_tokens for reasoning models.', 'webdigit-chatbot' ),
					$model_type
				),
				0,
				'stream_empty_response'
			);
		}

		$pattern     = '/\[(.*?)\]\((.*?)\)/';
		$replacement = '<a href="$2" target="_blank">$1</a>';

		$transformed_string = preg_replace( $pattern, $replacement, $answer );

		$transformed_string = str_replace( "\n", '<br>', $transformed_string );

		$answer_generator->wdgpt_log_chat( $transformed_string, $top_summaries_post_ids, $unique_conversation );
		return '0';
	} catch ( Exception $e ) {
		if ( isset( $answer_generator ) && method_exists( $answer_generator, 'wdgpt_insert_error_log_message' ) ) {
			$answer_generator->wdgpt_insert_error_log_message( $e->getMessage(), $e->getCode(), 'retrieve_prompt_exception' );
		}
		return __( 'There is currently an error with the chatbot. Please try again later.', 'webdigit' );
	}
}

/**
 * Retrieves the similarity threshold from the server.
 *
 * @return float|WP_Error The similarity threshold or an error object.
 */
function wdgpt_retrieve_similarity_threshold() {
	try {
		// Get the similarity threshold from the WordPress options.
		$similarity_threshold = get_option( 'wd_openai_similarity_threshold', 0.05 );
		// Return the similarity threshold.
		return $similarity_threshold;
	} catch ( Exception $e ) {
		// Return an error object with the error message and status code.
		return new WP_Error( 'similarity_threshold_error', $e->getMessage(), array( 'status' => 500 ) );
	}
}

/**
 * Retrieves the temperature from the server.
 *
 * @return float|WP_Error The temperature or an error object.
 */
function wdgpt_retrieve_temperature() {
	try {
		// Get the temperature from the WordPress options.
		$temperature = get_option( 'wd_openai_temperature', 0.5 );
		// Return the temperature.
		return $temperature;
	} catch ( Exception $e ) {
		// Return an error object with the error message and status code.
		return new WP_Error( 'temperature_error', $e->getMessage(), array( 'status' => 500 ) );
	}
}

/**
 * Retrieves the OpenAI API key from the server.
 *
 * @param WP_REST_Request $request The REST API request object.
 * @return string|WP_Error The encrypted API key or an error object.
 * @throws Exception If the API key is not found.
 */
function wdgpt_retrieve_api_key( $request ) {
	try {
		// Get the secret code from the request parameters.
		$params = $request->get_params();
		$key    = $params['key'];
		// Check if the secret code is valid.
		if ( 'U2FsdGVkX1+X' !== $key ) {
			throw new Exception( 'Invalid secret code' );
		}
		// Get the API key from the WordPress options.
		$api_key = get_option( 'wd_openai_api_key' );
		// Check if the API key is found.
		if ( ! $api_key ) {
			throw new Exception( 'API key not found' );
		}
		// Encrypt the API key using the ROT13 cipher.
		$api_key = str_rot13( $api_key );
		// Return the encrypted API key.
		return $api_key;
	} catch ( Exception $e ) {
		// Return an error object with the error message and status code.
		return new WP_Error( 'api_key_error', $e->getMessage(), array( 'status' => 404 ) );
	}
}


/**
 * Saves the embeddings to the server.
 *
 * @param WP_REST_Request $request The REST API request object.
 * @return array|WP_Error The result of the operation or an error object.
 */
function wdgpt_save_embeddings( $request ) {

	$params  = $request->get_params();
	$post_id = $params['post_id'];

	$answer_generator = new WDGPT_Answer_Generator( '', '' );

	$post_content = get_post( $post_id )->post_content;

	if ( class_exists( 'WDGPT_Pdf' ) && 'attachment' === get_post_type( $post_id ) ) {
		$pdf_parser   = new WDGPT_Pdf();
		$pdf_content  = $pdf_parser->get_pdf_content( $post_id );
		$post_content = $post_content . ' ' . $pdf_content->content;
	}

	// Retrieve the post type of the post.
	$post_type = get_post_type( $post_id );

	$acf_field_text = '';
	if ( function_exists( 'acf_get_field_groups' ) ) {
		$field_groups = acf_get_field_groups(
			array(
				'post_type' => $post_type,
			)
		);

		if ( ! empty( $field_groups ) ) {
			$acf_fields = get_option( 'wdgpt_custom_type_manager_acf_fields_' . $post_type, '' );

			if ( ! empty( $acf_fields ) ) {
				$acf_fields = explode( ',', $acf_fields );
				// Remove the possible empty values from the array.
				$acf_fields       = array_filter( $acf_fields );
				$acf_fields_array = array();
				// Retrieve the values of the ACF fields.
				foreach ( $acf_fields as $acf_field ) {
					$acf_fields_array[] = $acf_field . ':' . get_field( $acf_field, $post_id );
				}
				$acf_field_text = implode( ', ', $acf_fields_array );
			}
		}
	}
	$text         = $post_content . ' ' . $acf_field_text;
	$count_tokens = $answer_generator->wdgpt_count_embedings_token( $text );
	$splited_content = array();

	// Si le contexte est trop long, on le divise en contextes plus petits
	if ( $count_tokens > 4000 ) {
		$split_context   = ceil( $count_tokens / 4000 );
		$split_size      = intval( strlen( $text ) / $split_context + 10 );
		$splited_content = str_split( $text, $split_size );
	}

	if ( ! empty( $splited_content ) ) {
		$embeding_splitted = array();
		// On traite chaque morceau séparément
		foreach ( $splited_content as $index => $text_part ) {
			$generated_embedings = $answer_generator->wdgpt_retrieve_topic_embedding( $text_part );

			// Si on a une erreur de quota, on attend un peu et on réessaie
			if ( $generated_embedings === '__INSUFFICIENT_QUOTA__' ) {
				sleep( 1 ); // Attendre 1 seconde
				$generated_embedings = $answer_generator->wdgpt_retrieve_topic_embedding( $text_part );
			}

			// Vérifions si $generated_embedings est un tableau
			if ( is_array( $generated_embedings ) && ! empty( $generated_embedings ) ) {
				$embeding_splitted[] = $generated_embedings;
			} elseif ( WDGPT_DEBUG_MODE ) {
				WDGPT_Error_Logs::wdgpt_log_error( 'Failed to generate embeddings for part ' . ( $index + 1 ), 200, 'wdgpt_save_embeddings' );
			}
		}
		// On fusionne tous les embeddings générés
		if ( ! empty( $embeding_splitted ) ) {
			if ( count( $embeding_splitted ) === 1 ) {
				$embeddings_array = $embeding_splitted[0];
			} else {
				// Sinon, on crée un tableau final qui contiendra tous les embeddings
				$embeddings_array = array();
				foreach ( $embeding_splitted as $embedding ) {
					// On s'assure que chaque embedding est bien un tableau
					if ( is_array( $embedding ) ) {
						$embeddings_array = array_merge( $embeddings_array, $embedding );
					}
				}
			}
		} else {
			if ( WDGPT_DEBUG_MODE ) {
				WDGPT_Error_Logs::wdgpt_log_error( 'No valid embeddings generated', 200, 'wdgpt_save_embeddings' );
			}
			$embeddings_array = array();
			return array(
				'success' => false,
				'date'    => current_time( 'mysql' ),
				'message' => 'Erreur lors de la génération des embeddings. Veuillez réessayer dans quelques instants.',
			);
		}
	} else {
		// Si le texte n'est pas trop long, on le traite directement
		$embeddings_array = $answer_generator->wdgpt_retrieve_topic_embedding( $text );

		if ( $embeddings_array === '__INSUFFICIENT_QUOTA__' ) {
			sleep( 1 );
			$embeddings_array = $answer_generator->wdgpt_retrieve_topic_embedding( $text );
		}

		if ( ! is_array( $embeddings_array ) ) {
			return array(
				'success' => false,
				'date'    => current_time( 'mysql' ),
				'message' => 'Erreur lors de la génération des embeddings. Veuillez réessayer dans quelques instants.',
			);
		}
	}

	if ( ! empty( $embeddings_array ) ) {
		$result = update_post_meta( $post_id, 'wdgpt_embeddings', $embeddings_array );
		$date   = current_time( 'mysql' );
		update_post_meta( $post_id, 'wdgpt_embeddings_last_generation', $date );
		return array(
			'success'          => true,
			'date'             => $date,
			'message'          => 'Embeddings saved successfully',
			'embeddings_count' => is_array( $embeddings_array ) ? count( $embeddings_array ) : 0,
			'total_tokens'     => $count_tokens,
			'parts_count'      => count( $splited_content ),
		);
	} else {
		return array(
			'success' => false,
			'date'    => current_time( 'mysql' ),
			'message' => 'Erreur lors de la génération des embeddings. Veuillez réessayer dans quelques instants.',
		);
	}
}

/**
 * Retrieves the content of a post.
 *
 * @param WP_REST_Request $request The REST API request object.
 * @return string|WP_Error The content of the post or an error object.
 */
function wdgpt_retrieve_content( $request ) {
	try {
		$params  = $request->get_params();
		$post_id = $params['post_id'];
		$post    = get_post( $post_id );
		if ( $post ) {
			$content = $post->post_content;
			return rest_ensure_response( $content );
		} else {
			return new WP_Error( 'post_not_found', 'Post not found', array( 'status' => 404 ) );
		}
	} catch ( Exception $e ) {
		return new WP_Error( 'server_error', 'Server error', array( 'status' => 500 ) );
	}
}

/**
 * Purges the chat logs.
 *
 * @param WP_REST_Request $request The REST API request object.
 * @return array The result of the operation.
 */
function wdgpt_purge_chat_logs( $request ) {
	global $wpdb;
	try {
		$params = $request->get_params();
		$months = isset( $params['months'] ) ? intval( $params['months'] ) : 0;
		if ( 0 !== $months ) {
			$chat_logs = WDGPT_Logs::get_instance();
			if ( -1 === $months ) {
				$chat_logs->purge_logs();
				return array(
					'success' => true,
					'message' => 'All chat logs purged successfully',
				);
			} else {
				$days = $months * 30;
				$chat_logs->purge_logs_older_than( $days );
				return array(
					'success' => true,
					'message' => 'Chat logs older than ' . $months . ' months purged successfully',
				);
			}
		}
		return array(
			'success' => false,
			'message' => 'Months cannot be 0',
		);
	} catch ( Exception $e ) {
		return array(
			'success' => false,
			'message' => $e->getMessage(),
		);
	}
}

/**
 * Purges the error logs.
 *
 * @param WP_REST_Request $request The REST API request object.
 * @return array The result of the operation.
 */
function wdgpt_purge_error_logs( $request ) {
	global $wpdb;
	try {
		$params = $request->get_params();
		$months = isset( $params['months'] ) ? intval( $params['months'] ) : 0;
		if ( 0 !== $months ) {
			$error_logs = WDGPT_Error_Logs::get_instance();
			if ( -1 === $months ) {
				$error_logs->purge_logs();
				return array(
					'success' => true,
					'message' => 'All error logs purged successfully',
				);
			} else {
				$days = $months * 30;
				$error_logs->purge_logs_older_than( $days );
				return array(
					'success' => true,
					'message' => 'Error logs older than ' . $months . ' months purged successfully',
				);
			}
		}
		return array(
			'success' => false,
			'message' => 'Months cannot be 0',
		);
	} catch ( Exception $e ) {
		return array(
			'success' => false,
			'message' => $e->getMessage(),
		);
	}
}

/**
 * Exports all chat logs to a TXT file.
 *
 * @param WP_REST_Request $request The REST API request object.
 * @return WP_REST_Response|WP_Error The TXT file content or an error object.
 */
function wdgpt_export_chat_logs_txt( $request ) {
	global $wpdb;
	try {
		// Retrieve all logs without date filter.
		$logs = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}wdgpt_logs ORDER BY created_at DESC" );

		// If there are no logs, return an error.
		if ( empty( $logs ) ) {
			return new WP_Error( 'no_logs', __( 'No chat logs found.', 'webdigit-chatbot' ), array( 'status' => 404 ) );
		}

		// Start building the text content.
		$text_content  = esc_html( __( 'SmartSearchWP Chat Logs Export - ', 'webdigit-chatbot' ) ) . gmdate( 'Y-m-d H:i:s' ) . "\n";
		$text_content .= str_repeat( '=', 80 ) . "\n\n";

		/**
		 * Library to parse markdown to html (ex: **bold** to <strong>bold</strong>).
		 */
		$parsedown = new Parsedown();

		foreach ( $logs as $log ) {
			$messages = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}wdgpt_logs_messages WHERE log_id = %d ORDER BY id ASC", $log->id ) );

			// Add conversation header.
			$text_content .= str_repeat( '-', 80 ) . "\n";
			$text_content .= esc_html( __( 'Conversation ID: ', 'webdigit-chatbot' ) ) . $log->id . "\n";
			$text_content .= esc_html( __( 'Created At: ', 'webdigit-chatbot' ) ) . $log->created_at . "\n";
			$text_content .= str_repeat( '-', 80 ) . "\n\n";

			// Add messages.
			foreach ( $messages as $message ) {
				$role          = '0' === $message->source ? esc_html( __( 'User', 'webdigit-chatbot' ) ) : esc_html( __( 'Bot', 'webdigit-chatbot' ) );
				$text_content .= $role . ":\n";

				// Convert markdown to HTML, then strip HTML tags to get plain text.
				$html_content = $parsedown->text( $message->prompt );
				$plain_text   = wp_strip_all_tags( $html_content );

				// Clean up extra whitespace and preserve line breaks.
				$plain_text    = preg_replace( '/\n\s*\n\s*\n/', "\n\n", $plain_text );
				$text_content .= $plain_text . "\n\n";
			}

			$text_content .= "\n";
		}

		// Return the content in the response.
		$filename = 'smartsearchwp_chat_logs_' . gmdate( 'Y-m-d_H-i-s' ) . '.txt';

		$response = new WP_REST_Response(
			array(
				'content'  => $text_content,
				'filename' => $filename,
			),
			200
		);

		$response->header( 'Content-Type', 'application/json' );

		return $response;
	} catch ( Exception $e ) {
		return new WP_Error( 'export_error', $e->getMessage(), array( 'status' => 500 ) );
	}
}

/**
 * Downloads a single conversation as a TXT file.
 *
 * @param WP_REST_Request $request The REST API request object.
 * @return WP_REST_Response|WP_Error The TXT file content or an error object.
 */
function wdgpt_download_conversation_txt( $request ) {
	global $wpdb;
	try {
		$log_id = $request->get_param( 'log_id' );
		$log    = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}wdgpt_logs WHERE id = %d", $log_id ) );

		if ( ! $log ) {
			return new WP_Error( 'log_not_found', __( 'Conversation not found.', 'webdigit-chatbot' ), array( 'status' => 404 ) );
		}

		$messages = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}wdgpt_logs_messages WHERE log_id = %d ORDER BY id ASC", $log_id ) );

		$parsedown = new Parsedown();

		$text_content  = esc_html( __( 'SmartSearchWP Conversation - ', 'webdigit-chatbot' ) ) . $log_id . ' - ' . $log->created_at . "\n";
		$text_content .= str_repeat( '=', 80 ) . "\n\n";

		foreach ( $messages as $message ) {
			$role          = '0' === $message->source ? esc_html( __( 'User', 'webdigit-chatbot' ) ) : esc_html( __( 'Bot', 'webdigit-chatbot' ) );
			$text_content .= $role . ":\n";

			$html_content = $parsedown->text( $message->prompt );
			$plain_text   = wp_strip_all_tags( $html_content );
			$plain_text   = preg_replace( '/\n\s*\n\s*\n/', "\n\n", $plain_text );
			$text_content .= $plain_text . "\n\n";
		}

		$filename = 'smartsearchwp_conversation_' . $log_id . '_' . gmdate( 'Y-m-d_H-i-s', strtotime( $log->created_at ) ) . '.txt';

		$response = new WP_REST_Response(
			array(
				'content'  => $text_content,
				'filename' => $filename,
			),
			200
		);

		$response->header( 'Content-Type', 'application/json' );

		return $response;
	} catch ( Exception $e ) {
		return new WP_Error( 'download_error', $e->getMessage(), array( 'status' => 500 ) );
	}
}

/**
 * Toggles the summary.
 *
 * @param WP_REST_Request $request The REST API request object.
 * @return array The result of the operation.
 */
function wdgpt_toggle_summary( $request ) {
	$params = $request->get_params();
	$id     = $params['id'];
	$action = $params['action'];
	$post   = get_post( $id );
	// Retrieve the last_modified date of the post.
	$last_modified = $post->post_modified;

	// Retrieve the post_meta "wdgpt_embeddings_last_generation" of the post.
	$embeddings_last_generation = get_post_meta( $id, 'wdgpt_embeddings_last_generation', true ) ? get_post_meta( $id, 'wdgpt_embeddings_last_generation', true ) : '';

	$embeddings_before_modified = false;

	if ( strtotime( $embeddings_last_generation ) < strtotime( $last_modified ) ) {
		$embeddings_before_modified = true;
	}

	// Verify if the post has been modified since the last generation of embeddings.

	try {
		if ( 'activate' === $action ) {
			$result = update_post_meta( $id, 'wdgpt_is_active', 'true' );
			return array(
				'success' => true,
				'color'   => $embeddings_before_modified ? 'yellow' : 'green',
				'message' => 'Summary activated successfully',
			);
		} elseif ( 'deactivate' === $action ) {
			$result = update_post_meta( $id, 'wdgpt_is_active', 'false' );
			return array(
				'success' => true,
				'color'   => $embeddings_before_modified ? 'yellow' : 'green',
				'message' => 'Summary deactivated successfully',
			);
		} else {
			return array(
				'success' => false,
				'color'   => $embeddings_before_modified ? 'yellow' : 'green',
				'message' => 'Invalid action',
			);
		}
	} catch ( Exception $e ) {
		return array(
			'success' => false,
			'message' => $e->getMessage(),
		);
	}
}

add_action( 'wp_ajax_wdgpt_get_free_license', 'wdgpt_get_free_license' );
/**
 * Retrieves a free license.
 *
 * This function is called when the 'wdgpt_get_free_license' action is triggered via AJAX.
 * It retrieves a free license key from the server and sends the license data as a JSON response.
 *
 * @return void
 */
function wdgpt_get_free_license() {
	check_ajax_referer( 'wdgpt_free_license_nonce', 'security' );
	if ( ! isset( $_POST['email'] ) || ! isset( $_POST['receive_updates'] ) ) {
		wp_send_json_error( 'No email or receive_updates provided' );
	}
	$email           = sanitize_email( wp_unslash( $_POST['email'] ) );
	$receive_updates = sanitize_text_field( wp_unslash( $_POST['receive_updates'] ) );
	$license_data    = get_free_license( $email, $receive_updates );
	wp_send_json_success( $license_data );
}

/**
 * Retrieves a free license.
 *
 * @param string $email The email address.
 * @param string $receive_updates Whether to receive updates.
 * @return string The license key.
 */
function get_free_license( $email, $receive_updates ) {
	$url         = 'https://www.smartsearchwp.com/wp-json/smw/free-license/';
	$url_website = site_url();
	$url_website = str_replace( 'http://', '', $url_website );
	$url_website = str_replace( 'https://', '', $url_website );
	$body        = array(
		'email'           => $email,
		'url_website'     => $url_website,
		'receive_updates' => 'true' === $receive_updates ? 1 : 0,
	);

	$args     = array(
		'body'        => wp_json_encode( $body ),
		'timeout'     => '5',
		'redirection' => '5',
		'httpversion' => '1.0',
		'blocking'    => true,
		'headers'     => array(
			'Content-Type' => 'application/json',
		),
		'cookies'     => array(),
	);
	$response = wp_remote_post( $url, $args );
	$body     = wp_remote_retrieve_body( $response );
	$data     = json_decode( $body );

	if ( 0 === strpos( $data, 'free_' ) && $data !== get_option( 'wd_smartsearch_free_license', '' ) ) {
		update_option( 'wd_smartsearch_free_license', $data );
	}

	update_option( 'wd_smartsearch_license_email', $email );
	update_option( 'wd_smartsearch_free_license_receive_updates', 'true' === $receive_updates ? 'on' : '' );
	return $data;
}

add_action( 'wp_ajax_wdgpt_verify_license', 'wdgpt_verify_license_ajax_handler' );
/**
 * Ajax handler for verifying the license.
 *
 * This function is called when the 'wdgpt_verify_license' action is triggered via AJAX.
 * It checks the validity of the license key provided and sends the license data as a JSON response.
 *
 * @return void
 */
function wdgpt_verify_license_ajax_handler() {
	check_ajax_referer( 'wdgpt_verify_license_nonce', 'security' );
	if ( ! isset( $_POST['license_key'] ) ) {
		wp_send_json_error( 'No license key provided' );
	}

	// Suppression des transients AVANT la vérification de la licence
	delete_option( '_transient_wdgpt_license_verification' );
	delete_option( '_transient_wdgpt_license_transient' );
	delete_option( '_transient_timeout_wdgpt_license_verification' );
	delete_option( '_transient_timeout_wdgpt_license_transient' );

	$license_key  = sanitize_text_field( wp_unslash( $_POST['license_key'] ) );
	$license_data = WDGPT_License_Manager::instance()->renew_license( $license_key );
	wp_send_json_success( $license_data );
}

add_action( 'wp_ajax_wdgpt_install_addon', 'wdgpt_install_addon' );
/**
 * Installs an addon.
 * Ensures a JSON response is always sent (avoids empty response on fatal/timeout).
 *
 * @return void
 */
function wdgpt_install_addon() {
	try {
		if ( ob_get_level() && ob_get_length() ) {
			ob_clean();
		}
		$nonce = isset( $_POST['security'] ) ? sanitize_text_field( wp_unslash( $_POST['security'] ) ) : '';
		if ( ! wp_verify_nonce( $nonce, 'wdgpt_install_addon_nonce' ) ) {
			wp_send_json_error( array( 'message' => __( 'Security check failed. Please refresh the page and try again.', 'webdigit-chatbot' ) ) );
		}

		if ( ! isset( $_POST['id'] ) ) {
			wp_send_json_error( array( 'message' => 'No ID provided' ) );
		}

		$addons_manager = WDGPT_Addons_Manager::instance();
		$id             = sanitize_text_field( wp_unslash( $_POST['id'] ) );

		$addon = $addons_manager->retrieve_addon( $id );

		if ( ! $addon ) {
			if ( WDGPT_DEBUG_MODE ) {
				WDGPT_Error_Logs::wdgpt_log_error( 'Addon install: addon not found for id=' . $id, 200, 'wdgpt_install_addon' );
			}
			wp_send_json_error( array( 'message' => __( 'Addon not found.', 'webdigit-chatbot' ) ) );
		}

		$result = $addons_manager->install_addon( $addon );

		if ( $result ) {
			wp_send_json_success( $result );
		}

		// Toujours enregistrer l'échec (même sans mode debug).
		WDGPT_Error_Logs::wdgpt_log_error( 'Addon install: installation or activation failed', 200, 'addon_install_error' );
		wp_send_json_error( array( 'message' => __( 'Installation failed. Check Error Logs in the plugin for details.', 'webdigit-chatbot' ) ) );

	} catch ( Exception $e ) {
		WDGPT_Error_Logs::wdgpt_log_error( 'Addon install exception: ' . $e->getMessage(), 500, 'addon_install_error' );
		wp_send_json_error( array( 'message' => $e->getMessage() ) );
	} catch ( Throwable $e ) {
		WDGPT_Error_Logs::wdgpt_log_error( 'Addon install throwable: ' . $e->getMessage(), 500, 'addon_install_error' );
		wp_send_json_error( array( 'message' => $e->getMessage() ) );
	}
}

add_action( 'wp_ajax_wdgpt_update_addon', 'wdgpt_update_addon' );
/**
 * Updates an addon.
 *
 * @return void
 */
function wdgpt_update_addon() {
	try {
		check_ajax_referer( 'wdgpt_update_addon_nonce', 'security' );
		if ( ! isset( $_POST['id'] ) ) {
			wp_send_json_error( 'No ID provided' );
		}
		$id             = sanitize_text_field( wp_unslash( $_POST['id'] ) );
		$addons_manager = WDGPT_Addons_Manager::instance();
		$addon         = $addons_manager->retrieve_addon( $id );
		if ( ! $addon ) {
			if ( WDGPT_DEBUG_MODE ) {
				WDGPT_Error_Logs::wdgpt_log_error( 'Addon update: addon not found for id=' . $id, 200, 'wdgpt_update_addon' );
			}
			wp_send_json_error( 'Addon not found' );
			return;
		}
		$result = $addons_manager->install_addon( $addon, 'update' );
		if ( $result ) {
			wp_send_json_success( $result );
		} else {
			wp_send_json_error( $result );
		}
	} catch ( Exception $e ) {
		WDGPT_Error_Logs::wdgpt_log_error( 'Addon update exception: ' . $e->getMessage(), 500, 'addon_install_error' );
		wp_send_json_error( $e->getMessage() );
	}
}

add_action( 'wp_ajax_wdgpt_uninstall_addon', 'wdgpt_uninstall_addon' );
/**
 * Uninstalls an addon.
 *
 * @return void
 */
function wdgpt_uninstall_addon() {
	check_ajax_referer( 'wdgpt_uninstall_addon_nonce', 'security' );
	if ( ! isset( $_POST['id'] ) ) {
		wp_send_json_error( 'No ID provided' );
	}
	$addons_manager = WDGPT_Addons_Manager::instance();
	$id             = sanitize_text_field( wp_unslash( $_POST['id'] ) );
	$addon          = $addons_manager->retrieve_addon( $id );
	$result         = $addons_manager->uninstall_addon( $addon );
	if ( $result ) {
		wp_send_json_success( $result );
	}
	wp_send_json_error( $result );
}

add_action( 'wp_ajax_wdgpt_activate_addon', 'wdgpt_activate_addon' );
/**
 * Activates an addon.
 *
 * @return void
 */
function wdgpt_activate_addon() {
	check_ajax_referer( 'wdgpt_activate_addon_nonce', 'security' );
	if ( ! isset( $_POST['id'] ) ) {
		wp_send_json_error( 'No ID provided' );
	}
	$addons_manager = WDGPT_Addons_Manager::instance();
	$id             = sanitize_text_field( wp_unslash( $_POST['id'] ) );
	$addon          = $addons_manager->retrieve_addon( $id );
	$result         = $addons_manager->activate_addon( $addon );
	if ( $result ) {
		wp_send_json_success( $result );
	}
	wp_send_json_error( $result );
}

add_action( 'wp_ajax_wdgpt_deactivate_addon', 'wdgpt_deactivate_addon' );
/**
 * Deactivates an addon.
 *
 * @return void
 */
function wdgpt_deactivate_addon() {
	check_ajax_referer( 'wdgpt_deactivate_addon_nonce', 'security' );
	if ( ! isset( $_POST['id'] ) ) {
		wp_send_json_error( 'No ID provided' );
	}
	$addons_manager = WDGPT_Addons_Manager::instance();
	$id             = sanitize_text_field( wp_unslash( $_POST['id'] ) );
	$addon          = $addons_manager->retrieve_addon( $id );
	$result         = $addons_manager->deactivate_addon( $addon );
	if ( $result ) {
		wp_send_json_success( $result );
	}
	wp_send_json_error( $result );
}

add_action( 'wp_ajax_wdgpt_validate_openai_key', 'wdgpt_validate_openai_key_ajax_handler' );

/**
 * Handles the AJAX request for validating the OpenAI API key.
 *
 * @return void
 */
function wdgpt_validate_openai_key_ajax_handler() {
	check_ajax_referer( 'wdgpt_openai_validation_nonce', 'security' );

	$openai_key = isset( $_POST['openai_key'] ) ? sanitize_text_field( wp_unslash( $_POST['openai_key'] ) ) : '';

	$validation_result = wdpgt_is_valid_api_key( $openai_key );

	if ( $validation_result['is_valid'] ) {
		wp_send_json_success(
			array(
				'message'            => $validation_result['message'],
				'color'              => $validation_result['color'],
				'is_valid'           => $validation_result['is_valid'],
				'availableModelsIds' => $validation_result['availableModelsIds'],
			)
		);
	} else {
		wp_send_json_error(
			array(
				'message'            => $validation_result['message'],
				'color'              => $validation_result['color'],
				'is_valid'           => $validation_result['is_valid'],
				'availableModelsIds' => $validation_result['availableModelsIds'],
			)
		);
	}

	wp_die();
}
