<?php
/**
 * Escáner de contenido para el plugin Compibot.
 *
 * Este archivo contiene la clase `CompibotScanner`, encargada de escanear páginas, entradas y productos
 * en un sitio WordPress y enviar los datos recopilados a la API de Compibot.
 *
 * @package Compibot
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

define( 'COMPIBOT_API_RAG_ENDPOINT', 'rag/' );
define( 'COMPIBOT_API_SET_TOTAL_ENDPOINT', 'set-total/' );
if ( ! defined( 'COMPIBOT_API_BASE_URL' ) ) {
	define( 'COMPIBOT_API_BASE_URL', 'https://panel.compibot.com/api/' ); 
}


/**
 * Clase CompiBotScanner
 *
 * Clase encargada de escanear páginas, entradas y productos en un sitio WordPress
 * y envian los datos recopilados a la API externa de panel.
 */
class CompibotScanner {


	/**
	 * Objeto global de la base de datos de WordPress
	 *
	 * @var object $wpdb Objeto global de la base de datos de WordPress.
	 */
	private $wpdb;

	/**
	 * Nombre de la tabla de Compibot
	 *
	 * @var string $table_name Nombre de la tabla personalizada del plugin en la base de datos (X_compibot).
	 */
	private $table_name;

	/**
	 * Token de autenticación
	 *
	 * @var string $token Token de autenticación para poder realizar las llamadas APIs.
	 */
	private $token;

	/**
	 * Constructor de la clase
	 *
	 * Inicializa las propiedades necesarias para interacturar con la base de datos
	 * y establece el token de autenticación
	 *
	 * @param string $token Token de autenticación para la API.
	 */
	public function __construct( $token ) {
		global $wpdb;
		$this->wpdb       = $wpdb;
		$this->table_name = $wpdb->prefix . 'compibot';
		$this->token      = $token;
	}


	/**
	 * Método que escanea TODAS las páginas del sitio WordPress donde se encuentra instalado el plugin.
	 *
	 * @return array Listado de páginas escaneadas.
	 */
	public function compibot_scan_pages_batch() {
		$excluded_slugs = array(); // Slugs que se excluirán del escaneo.
		$batch_size     = 50; // Tamaño del lote.
		$offset         = 0;      // Inicio del desplazamiento.
		$kb_scanned     = 0;  // Tamaño escaneado.
		$kb_total       = 0;    // Tamaño total acumulado.
		$pages_array    = array(); // Array que contendrá los datos escaneados.

		// Bucle infinito para recorrer las páginas en lotes.
		while ( true ) {
			// Log para saber el offset y batch_size actual.

			// Obtenemos las páginas en lotes.
			$pages = $this->compibot_get_pages_batch( $offset, $batch_size );

			// Si no hay más páginas, salir del bucle.
			if ( empty( $pages ) ) {
				break;
			}

			// Procesar cada página que se ha obtenido.
			foreach ( $pages as $page ) {
				$page_info = $this->compibot_process_content(
					$page,
					$excluded_slugs,
					$kb_scanned,
					$kb_total
				);

				if ( $page_info ) {
					$pages_array[] = $page_info;
				}
			}

			// Aumentamos el desplazamiento para la siguiente iteración.
			$offset += $batch_size;
		}

		return $pages_array; // Devolvemos los datos escaneados para que se procesen en otro lugar.
	}



	/**
	 * Método que escanea todas las publicaciones (entradas/posts) del sitio WordPress donde se encuentra instalado el plugin y lo hace por lotes
	 *
	 * @return array Listado de publicaciones escaneadas.
	 */
	public function compibot_scan_posts_batch() {

		$batch_size  = 50; // Tamaño del lote.
		$offset      = 0; // Inicio del desplazamiento.
		$kb_scanned  = 0;
		$kb_total    = 0;
		$posts_array = array(); // Array que contendrá los datos escaneados.

		while ( true ) {
			// Mientras que esté en el bucle, obtenemos un lote de publicaciones.
			$posts = $this->compibot_get_all_posts_batch( $offset, $batch_size );

			if ( empty( $posts ) ) {
				break; // Si no hay más publicaciones, salimos del bucle.
			}

			foreach ( $posts as $post ) {
				$post_info = $this->compibot_process_content(
					$post,
					array(),
					$kb_scanned,
					$kb_total
				);

				if ( $post_info ) {
					$posts_array[] = $post_info;
				}

				// Liberar memoria del post procesado.
				unset( $post_info );
			}
			unset( $posts ); // Liberar memoria del lote actual.
			$offset += $batch_size; // Aumentamos el desplazamiento para la siguiente iteración.
		}

		return $posts_array; // Devolvemos los datos escaneados para procesarlos en otro método.
	}

	/**
	 * Método que escanea todos los productos del sitio si WooCommerce está activo usando un sistema por LOTES (Batch)
	 *
	 * Con esto tratamos de conseguir no romper el límite de tiempo de ejecución de PHP o el límite de memoria del servidor.
	 *
	 * @return array Listado de productos escaneados.
	 */
	public function compibot_scan_products_batch() {
		// TODO: Investigar.
		if ( ! $this->compibot_is_woocommerce_active() ) {
			return array();
		}

		$batch_size     = 50; // Tamaño del lote.
		$offset         = 0; // Inicio del desplazamiento.
		$products_array = array(); // Array para almacenar los datos escaneados.

		do {
			$args = array(
				'post_type'      => 'product',
				'posts_per_page' => $batch_size,
				'offset'         => $offset,
				'post_status'    => 'publish',
			);

			$query = new WP_Query( $args );

			while ( $query->have_posts() ) {
				$query->the_post();
				$product           = wc_get_product( get_the_ID() );
				$fecha_publicacion = get_the_date( 'd-m-Y H:i:s', get_the_ID() );
				$on_sale           = $product->is_on_sale() ? 'Sí' : 'No';
				$products_array[]  = array(
					'info'  => 'Nombre: ' . wp_strip_all_tags( strip_shortcodes( get_the_title() ) ) .
								' | Precio: ' . wp_strip_all_tags( strip_shortcodes( wc_price( $product->get_price() ) ) ) .
								' | En oferta: ' . $on_sale .
								' | URL: ' . get_permalink() .
								' | Descripción: ' . wp_strip_all_tags( strip_shortcodes( get_the_excerpt() ) ),
					'fecha' => $fecha_publicacion,
				);
			}

			$offset += $batch_size; // Incrementa el desplazamiento para el siguiente lote.
			wp_reset_postdata();
		} while ( $query->found_posts > $offset ); // Continua mientras haya más resultados.

		return $products_array; // Devuelve los datos escaneados.
	}






	/**
	 * Método que procesa el contenido de una publicacion (entrada/post) o página.
	 *
	 * @param  object $item           Objeto de la publicacion o página a
	 *                                procesar.
	 * @param  array  $excluded_slugs Slugs excluidos del escaneo.
	 * @param  float  &$kb_scanned    Referencia al tamaño total en
	 *                                KB escaneado.
	 * @param  float  &$kb_total      Referencia al tamaño total en
	 *                                KB acumulados.
	 * @return string|null Información procesada o null si el slug está excluido.
	 */
	private function compibot_process_content( $item, $excluded_slugs, &$kb_scanned, &$kb_total ) {
		$slug = get_post_field( 'post_name', $item->ID );

		if ( in_array( $slug, $excluded_slugs, true ) ) {
			return;
		}
		$content = apply_filters( 'the_content', $item->post_content ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound

		// Eliminar shortcodes.
		$clean_content = strip_shortcodes( $content );

		// Eliminar etiquetas HTML.
		$clean_content = wp_strip_all_tags( $clean_content );

		// Eliminar patrones específicos como [vc_separator ...].
		$clean_content = preg_replace( '/\[[^\]]+\]/', '', $clean_content );

		// (Opcional) Quitar espacios en blanco innecesarios.
		$clean_content = trim( $clean_content );

		$kb_size = round( strlen( $clean_content ) / 1024, 2 );

		$kb_scanned += $kb_size;
		$kb_total   += $kb_size;

		// Obtenemos la fecha de publicación del post/página.
		// See: https://developer.wordpress.org/reference/functions/get_the_date/.
		$fecha_publicacion = get_the_date( 'd-m-Y H:i:s', $item->ID );

		return array(
			'info'  => 'Titulo: ' . get_the_title( $item->ID ) .
						' | URL: ' . get_permalink( $item->ID ) .
						' | Resumen: ' . $clean_content,
			'fecha' => $fecha_publicacion,
		);
	}


	/**
	 * Método que finaliza el escaneo, actualiza la base de datos y envía los datos a la API.
	 *
	 * @param  array  $data_array Datos recopilados del escaneo.
	 * @param  string $scan_type  Tipo de escaneo (pages, posts, products).
	 * @param  float  $kb_scanned Tamaño total de KB
	 *                            escaneado.
	 * @param  float  $kb_total   Tamaño total de
	 *                            KB acumulado.
	 * @return void
	 */
	private function compibot_finalize_scan( $data_array, $scan_type, $kb_scanned, $kb_total ) {
		// TODO: Pendiente de observar si se usa este método.
		$replace_flag = ! empty( $data_array ) ? 1 : 0;

		// Actualizamos la base de datos.
		$this->wpdb->update(
			$this->table_name,
			array(
				'replace_flag'  => $replace_flag,
				'kb_scanned'    => round( $kb_scanned, 2 ),
				'kb_total'      => round( $kb_total, 2 ),
				'last_scan'     => current_time( 'mysql' ),
				'scanned_items' => count( $data_array ),
			),
			array( 'id' => 1 )
		);

		if ( $replace_flag ) {
			// Enviamos los datos a la API.
			$response = $this->compibot_send_to_api(
				COMPIBOT_API_RAG_ENDPOINT,
				array(
					'data_array' => $data_array,
					'replace'    => $replace_flag,
				)
			);
			$this->compibot_handle_api_response( $response, "$scan_type enviados" );

			// Enviar el total de elementos escaneados a la API.
			$total_count    = $this->compibot_get_total_count( $scan_type );
			$total_response = $this->compibot_send_to_api( COMPIBOT_API_SET_TOTAL_ENDPOINT, array( 'total' => $total_count ) );
			$this->compibot_handle_api_response( $response, "total de $scan_type enviados" );
		}
	}

	/**
	 * Método que obtiene todas las páginas publicadas en el sitio WordPress.
	 *
	 * @param  int $offset     Desplazamiento para la consulta.
	 * @param  int $batch_size Tamaño del lote.
	 * @return array Listado de páginas publicadas.
	 */
	private function compibot_get_pages_batch( $offset = 0, $batch_size = 50 ) {
		// 1) Guardamos el idioma que está activo antes de hacer el switch.
		$original_lang = null;
		if ( has_filter( 'wpml_current_language' ) ) {
			$original_lang = apply_filters( 'wpml_current_language', null ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
		}


		// 2) Cambiamos a 'all' para que WPML no aplique filtros de idioma.
		if ( has_action( 'wpml_switch_language' ) ) {
			do_action( 'wpml_switch_language', 'all' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
		}

		// 3) Construimos el WP_Query para traer páginas en estado "publish".
		// Si algunas traducciones tienen estado 'inherit', añade ['publish','inherit'].
		$args = array(
			'post_type'        => 'page',
			'post_status'      => 'publish',
			'orderby'          => 'date',
			'order'            => 'DESC',
			'posts_per_page'   => $batch_size,
			'offset'           => $offset,

			// Importante.
			'suppress_filters' => false,  // Permite que WPML reciba 'lang'.
			'lang'             => 'all',  // Forzamos que se incluyan todos los idiomas.
		);

		$query = new WP_Query( $args );

		// Guardamos las páginas recuperadas.
		$pages = $query->posts;

		// 4) Restauramos el idioma original.
		if ( $original_lang && has_action( 'wpml_switch_language' ) ) {
			do_action( 'wpml_switch_language', $original_lang ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
		}

		// Devolvemos el array de páginas.
		return $pages;
	}


	/**
	 * Método que obtiene todas las publicaciones (entradas/posts) publicadas en el sitio WordPress.
	 *
	 * @param  int $offset     Desplazamiento para la consulta.
	 * @param  int $batch_size Tamaño del lote.
	 * @return array Listado de publicaciones publicadas.
	 */
	private function compibot_get_all_posts_batch( $offset = 0, $batch_size = 50 ) {
		// Paso 1 - Guardamos el idioma actual por para restaurarlo después.
		$original_lang = apply_filters( 'wpml_current_language', null ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound

		// Paso 2 - Forzamos a WPML a mostrar "todos" los idiomas (no solo el activo).
		do_action( 'wpml_switch_language', 'all' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound

		// Paso 3 - Construimos el WP_Query para traer las publicaciones en estado "publish".
		$args = array(
			'post_type'        => 'post',
			'post_status'      => 'publish',
			'orderby'          => 'date',
			'order'            => 'DESC',
			'posts_per_page'   => $batch_size,
			'offset'           => $offset,

			// Claves para que WPML no aplique filtros de idioma - NO ESTOY SEGURO SI FUNCIONA.
			'suppress_filters' => false,
			'lang'             => 'all',
		);

		$query = new WP_Query( $args );
		$posts = $query->posts;

		// Paso 4 - Restauramos el idioma original.
		do_action( 'wpml_switch_language', $original_lang ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound

		return $posts;
	}

	/**
	 * Método que obtiene todos los productos de WooCommerce publicados en el sitio WordPress y los obtiene por lotes
	 *
	 * @param  int $offset     Desplazamiento para la consulta.
	 * @param  int $batch_size Tamaño del lote.
	 * @return array Listado de productos publicados y datos asociados.
	 */
	private function compibot_get_all_products_batch( $offset = 0, $batch_size = 50 ) {
		if ( ! $this->compibot_is_woocommerce_active() ) {
			return array();
		}

		$args = array(
			'post_type'      => 'product',
			'posts_per_page' => $batch_size,
			'offset'         => $offset,
			'post_status'    => 'publish',
		);

		$query          = new WP_Query( $args );
		$products_array = array();

		while ( $query->have_posts() ) {
			$query->the_post();
			$product = wc_get_product( get_the_ID() );

			$on_sale = $product->is_on_sale() ? 'Sí' : 'No';

			// Obtenemos la fecha de publicación del producto.
			// See: https://developer.wordpress.org/reference/functions/get_the_date/.
			$products_array[] = array(
				'info'  => 'Nombre: ' . get_the_title() .
							' | Precio: ' . wc_price( $product->get_price() ) .
							' | En oferta: ' . $on_sale .
							' | URL: ' . get_permalink() .
							' | Descripción: ' . get_the_excerpt(),
				'fecha' => get_the_date( 'd-m-Y H:i:s', get_the_ID() ),
			);
		}

		wp_reset_postdata();
		return $products_array;
	}


	/**
	 * Método que combina los datos de las páginas, publicaciones y productos escaneados y realiza un escaneo por lotes según el array recibido.
	 * comprime estos datos y los envía en una sola solicitud a la API
	 *
	 * @param  array $scan_types Array con los tipos de escaneo a realizar (pages, posts, woocommerce).
	 * @return void
	 */
	public function compibot_send_combined_data_batch( $scan_types ) {
		$start_time = microtime( true ); // Inicio del temporizador.

		$combined_array = array(); // Inicializamos el array que contendrá todos los datos escaneados.
		$kb_total       = 0;
		$kb_scanned     = 0;

		foreach ( $scan_types as $scan_type ) {
			switch ( $scan_type ) {
				case 'pages':
					$pages_array    = $this->compibot_scan_pages_batch();
					$combined_array = array_merge( $combined_array, $pages_array );
					$kb_scanned    += $this->calculate_total_kb( $pages_array );
					break;

				case 'posts':
					$posts_array    = $this->compibot_scan_posts_batch();
					$combined_array = array_merge( $combined_array, $posts_array );
					$kb_scanned    += $this->calculate_total_kb( $posts_array );
					break;

				case 'woocommerce':
					$products_array = $this->compibot_scan_products_batch();
					$combined_array = array_merge( $combined_array, $products_array );
					$kb_scanned    += $this->calculate_total_kb( $products_array );
					break;

				default:
					break;
			}
		}

		$kb_total += $kb_scanned;

		// Actualizar la base de datos.
		$replace_flag = ! empty( $combined_array ) ? 1 : 0;
		$this->wpdb->update(
			$this->table_name,
			array(
				'replace_flag'  => $replace_flag,
				'kb_scanned'    => round( $kb_scanned, 2 ),
				'kb_total'      => round( $kb_total, 2 ),
				'last_scan'     => current_time( 'mysql' ),
				'scanned_items' => count( $combined_array ),
			),
			array( 'id' => 1 )
		);

		// Comprimimos y enviamos los datos combinados.
		$response = $this->compibot_send_to_api(
			COMPIBOT_API_RAG_ENDPOINT,
			array(
				'data_array' => $combined_array,
				'replace'    => 1,
			)
		);

		if ( $response['success'] ) {
			$end_time = microtime( true ); // Fin del temporizador.
			$this->compibot_redirect_on_success( $response );
		}
	}


	/**
	 * Método que envía datos a un endpoint específico de la API.
	 *
	 * @param  string $endpoint Endpoint de la API al que se enviarán los datos.
	 * @param  array  $data     Datos a enviar al endpoint.
	 * @return mixed Respuesta de la API o false en el caso de error.
	 * @see    https://www.php.net/manual/en/function.gzencode.php
	 */
	private function compibot_send_to_api( $endpoint, $data ) {
		$memory_limit  = ini_get( 'memory_limit' ); // Establecemos el límite de memoria a 512 MB.
		$memory_limit  = intval( $memory_limit );
		$memoria_usada = round( memory_get_usage() / 1024 / 1024, 2 );

		if ( $memory_limit <= 1000 && $memoria_usada >= 300 ) {
			// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
			ini_set( 'memory_limit', '1024M' ); // 520 MB en lugar de 1024 MB.
		} elseif ( $memory_limit <= 520 && $memoria_usada >= 150 ) {
			// phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged
			ini_set( 'memory_limit', '550M' ); // 768 MB en lugar de 512 MB.
		}

		// Definimos el tamaño del chunk por defecto.
		$chunk_size = 10000000; // 10MB.

		// Para debug, definimos el tamaño a 8k.

		// Enviamos el cuerpo de la solicitud comprimido con gzencode.
		// 1. Debug antes/después del JSON.
		$json_data     = wp_json_encode( $data );
		$original_size = strlen( $json_data );

		// Comprimimos los datos con gzencode.
		// 2. Debug antes/después de comprimir.
		$compressed_data      = gzencode( $json_data, 9, FORCE_GZIP );
		$compressed_data_size = strlen( $compressed_data );

		// Liberar JSON original (ya no se necesita).
		unset( $json_data );

		// Nueva funcionalidad - Envío de datos por chunks a la API.
		// 3. Debug del chunking.
		$chunks    = str_split( $compressed_data, $chunk_size );
		$responses = array(); // Array para almacenar todas las respuestas.

		foreach ( $chunks as $index => $chunk ) {

			// 4. Debug del chunk actual.
			$response = wp_remote_post(
				COMPIBOT_API_BASE_URL . $endpoint,
				array(
					'method'  => 'POST',
					'body'    => $chunk,
					'headers' => array(
						'Content-Type'     => 'application/json',
						'Content-Encoding' => 'gzip', // Informamos que el contenido se encuentra comprimido.
						'Authorization'    => 'Bearer ' . $this->token,
						'X-Chunk-Index'    => $index, // Índice del fragmento.
						'X-Chunk-Total'    => count( $chunks ), // Total de fragmentos.
					),
					'timeout' => 30,
				)
			);

					// Liberar chunk procesado.
			unset( $chunk );

			// Manejamos la respuesta de la API para el fragmento actual.
			if ( ! $this->compibot_handle_api_response( $response, 'Chunk ' . ( $index + 1 ) . '/' . count( $chunks ) ) ) {
				return array(
					'success'   => false,
					'error'     => 'Error al enviar el chunk ' . ( $index + 1 ),
					'responses' => $responses,
				); // Retorna todas las respuestas hasta el fallo.
			}

			// Guardar la respuesta para referencia futura.
			$responses[] = array(
				'chunk_index' => $index,
				'response'    => wp_remote_retrieve_body( $response ),
			);

			// Liberar respuesta.
			unset( $response );
		}

		// Retornar todas las respuestas si todos los fragmentos se envían correctamente.
		return array(
			'success'   => true,
			'responses' => $responses,
		);
	}

	/**
	 * Método que maneja la respuesta de la API y registra errores si ocurren.
	 *
	 * @param  mixed  $response Respuesta de la API.
	 * @param  string $context  Contexto para el log (ej. 'envío de
	 *                          páginas').
	 * @return bool true si la respuesta es correcta, false en caso contrario.
	 */
	private function compibot_handle_api_response( $response, $context ) {
		if ( is_wp_error( $response ) ) {
			wp_die( esc_html( "Error en la API ($context): " . esc_html( $response->get_error_message() ) ) );
			return false;
		}

		$response_code = wp_remote_retrieve_response_code( $response );
		if ( 200 !== $response_code ) {
			wp_die( esc_html( "Error en la API ($context): Código de respuesta $response_code" ) );
			return false;
		}

		return wp_remote_retrieve_body( $response );
	}

	/**
	 * Método que verifica si WooCommerce está activo en el sitio WordPress.
	 *
	 * @return bool true si WooCommerce está activo, false en caso contrario.
	 */
	private function compibot_is_woocommerce_active() {
		if ( ! function_exists( 'is_plugin_active' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}

		return is_plugin_active( 'woocommerce/woocommerce.php' );
	}



	/**
	 * Método que calcula el tamaño total en KB de un array de datos.
	 *
	 * @param  array $data_array Array de datos a calcular.
	 * @return float Tamaño total en KB de los datos.
	 */
	private function calculate_total_kb( $data_array ) {
		$total_kb = 0;
		foreach ( $data_array as $data ) {
			if ( is_array( $data ) && isset( $data['info'] ) ) {
				$total_kb += round( strlen( $data['info'] ) / 1024, 2 );
			}
		}

		return $total_kb;
	}


	/**
	 * Método para obtener el número total de elementos que se tiene en el sitio WordPress.
	 *
	 * El número total de elementos va entre "WooCommerce, Páginas y Entradas del blog"
	 *
	 * @return int Número total de elementos.
	 */
	public function compibot_get_total_count() {
		$total_count = 0;

		// Obtenemos el número total de páginas.
		// Escapar el nombre de la tabla (sin usar prepare).
		$table_name = esc_sql( $this->wpdb->prefix . 'posts' );

		// Construir la consulta con el nombre de la tabla seguro.
		$query_pages = "SELECT COUNT(*) FROM `$table_name` WHERE post_type = %s AND post_status = %s";

		// Ejecutar la consulta con `prepare()` solo para los valores dinámicos.
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$pages_count = (int) $this->wpdb->get_var( $this->wpdb->prepare( $query_pages, 'page', 'publish' ) );

		// Obtenemos el número total de entradas.
		$query_posts = "SELECT COUNT(*) FROM `$table_name` WHERE post_type = %s AND post_status = %s";

        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$posts_count = (int) $this->wpdb->get_var( $this->wpdb->prepare( $query_posts, 'post', 'publish' ) );

		// Si WooCommerce está activo, obtenemos el número total de productos.
		$products_count = 0;
		if ( $this->compibot_is_woocommerce_active() ) {

			// Construir la consulta con el nombre de la tabla seguro.
			$query_products = "SELECT COUNT(*) FROM `$table_name` WHERE post_type = %s AND post_status = %s";

            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$products_count = (int) $this->wpdb->get_var( $this->wpdb->prepare( $query_products, 'product', 'publish' ) );
		}

		// Calculamos el total de elementos que tenemos.
		$total_count = $pages_count + $posts_count + $products_count;

		// Registramos el total para depuración.

		return $total_count;
	}

	/**
	 * Calcula el tamaño total en bytes de un conjunto de datos en formato JSON.
	 *
	 * Esta función divide los datos en fragmentos del tamaño especificado,
	 * los convierte a JSON y suma su tamaño total en bytes.
	 * Se usa para estimar el tamaño de los datos antes de enviarlos a una API.
	 *
	 * @param array $data       Datos a calcular.
	 * @param int   $chunk_size Tamaño de cada fragmento en elementos (por defecto 100).
	 * @return int  Tamaño total en bytes del JSON generado.
	 */
	private function calcular_tamaño_json_eficiente( $data, $chunk_size = 100 ) {
		$total_bytes = 0;

		foreach ( array_chunk( $data, $chunk_size ) as $chunk ) {
			$json_chunk   = wp_json_encode( $chunk );
			$total_bytes += strlen( $json_chunk );
			unset( $json_chunk ); // Liberar memoria del chunk.
		}

		return $total_bytes;
	}

	/**
	 * Método para manejar la redirección tras un escaneo exitoso.
	 *
	 * @param  mixed $response Respuesta de la API.
	 * @return void
	 */
	public function compibot_redirect_on_success( $response ) {

		// Decodificamos la respuesta JSON.
		$decoded_response = is_array( $response ) ? $response : json_decode( $response, true );

		// Extraemos el status si está presente.
		$response_status = 'ok'; // Ponemos un valor por defecto.

		if ( ! empty( $decoded_response['responses'][0]['response'] ) ) {

			// Verificamos si la subrespuesta es un string antes de decodificar.
			$inner_response = is_array( $decoded_response['responses'][0]['response'] )
				? $decoded_response['responses'][0]['response']
				: json_decode( $decoded_response['responses'][0]['response'], true );

			if ( ! empty( $inner_response['status'] ) ) {
				$response_status = $inner_response['status'];
			}
		}

		// Guarda el status en un transitorio por 5 minutos.
		set_transient( 'compibot_scan_status', $response_status, 30 );

		// Genera el nonce para validar la redirección.
		$nonce = wp_create_nonce( 'compibot_save_settings_action' );

		// Construye la URL de redirección con el nonce y el parámetro de éxito.
		$redirect_url = admin_url( 'admin.php?page=compibot' );

		// Redirige a la URL construida.
		wp_safe_redirect( $redirect_url );
		exit;
	}


	/**
	 * Método que obtiene los primeros 10 elementos escaneados desde la base de datos
	 * METODO MOCKUP
	 */
	public static function compibot_get_scanned_items() {

		global $wpdb;

		$db_manager = new CompibotDatabaseManager();

		// Obtenemos el tipo de scan que se ha realizado.
		$scan_type = $db_manager->compibot_select( 'scan_type', 'plugin_group_compibot', 60 );

		if ( ! $scan_type ) {
			wp_send_json_error( array( 'message' => 'No se ha realizado ningún escaneo' ) );
		}

		// Generamos una clave de caché única.
		$cache_key = 'compibot_scanned_items_' . md5( $scan_type );

		// Intentamos obtener los resultados desde la caché.
		$cached_results = wp_cache_get( $cache_key, 'compibot' );

		if ( false !== $cached_results ) {
			wp_send_json_success( $cached_results );
		}

		// Definimos los tipos de post en base al scan_type.
		$types      = explode( ',', $scan_type ); // Convertimos el string en un array.
		$post_types = array();

		if ( in_array( 'pages', $types, true ) ) {
			$post_types[] = 'page';
		}
		if ( in_array( 'posts', $types, true ) ) {
			$post_types[] = 'post';
		}
		if ( in_array( 'woocommerce', $types, true ) ) {
			$post_types[] = 'product';
		}

		if ( ! isset( $_POST['nonce_compibot_get_scanned_items'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce_compibot_get_scanned_items'] ) ), 'compibot_get_scanned_items' ) ) {
			wp_send_json_error( array( 'message' => 'Nonce inválido' ) );
			wp_die();
		}

		// Obtenemos el número de página actual desde la petición AJAX.
		$page = isset( $_POST['page'] ) ? intval( $_POST['page'] ) : 1;

		$items_per_page = 10; // Número de elementos por página.
		$offset         = ( $page - 1 ) * $items_per_page; // Cálculo del desplazamiento.

		$total_items = $db_manager->compibot_count_posts_by_type( $post_types );

		$table_name = $wpdb->prefix . 'posts';

		// Obtenemos el valor de la ordenación recogida desde el selector de HTML.
		$order_by = isset( $_POST['orderBy'] ) ? sanitize_text_field( wp_unslash( $_POST['orderBy'] ) ) : 'page';

		$items = $db_manager->compibot_get_scanned_items_sorted( $post_types, $order_by, $items_per_page, $offset );

		// Almacenar los resultados en caché durante 60 segundos.
		wp_cache_set( $cache_key, $response, 'compibot', 60 );

		// Devolvemos los datos en formato JSON.
		wp_send_json_success(
			array(
				'items'        => $items,
				'total_pages'  => ceil( $total_items / $items_per_page ),
				'current_page' => $page,
			)
		);
	}
}
