<?php
/**
 * Gestor de base de datos de Compibot.
 *
 * Esta clase se encarga de manejar las interacciones con la base de datos
 * para el almacenamiento y recuperación de datos relacionados con Compibot.
 *
 * @package Compibot
 */

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

/**
 * Clase para manejar la base de datos de Compibot en WordPress.
 */
class CompibotDatabaseManager {

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

	/**
	 * Nombre de la tabla utilizada para almacenar datos de Compibot.
	 *
	 * @var string
	 */
	private $table_name;

	/**
	 * Constructor de la clase CompibotDatabaseManager
	 *
	 * Se encarga de inicializar las variables de clase para obtener el objecto de la BD y el nombre de la tabla
	 */
	public function __construct() {
		global $wpdb;
		$this->wpdb       = $wpdb;
		$this->table_name = $this->wpdb->prefix . 'compibot';
		$this->table_name = esc_sql( $this->table_name );
	}


	/**
	 * Obtiene los elementos escaneados ordenados según el tipo de contenido.
	 *
	 * @param array  $post_types Tipos de post a buscar (e.g., 'page', 'post', 'product').
	 * @param string $order_by   Tipo de post con prioridad de ordenación.
	 * @param int    $items_per_page Cantidad de elementos por página.
	 * @param int    $offset Desplazamiento para la paginación.
	 * @return array Lista de elementos escaneados.
	 */
	public function compibot_get_scanned_items_sorted( $post_types, $order_by, $items_per_page, $offset ) {
		global $wpdb;

		// Validar que los tipos de post sean un array no vacío.
		if ( empty( $post_types ) || ! is_array( $post_types ) ) {
			return array();
		}

		// Definir y escapar el nombre de la tabla.
		$table_name = esc_sql( $wpdb->posts );

		// Crear placeholders dinámicos para `post_type IN (...)`.
		$placeholders = implode( ',', array_fill( 0, count( $post_types ), '%s' ) );

		// Validar o escapar la columna de ordenación.
		$order_case_column = esc_sql( $order_by );

		// Generar el ordenamiento CASE de forma segura.
		$order_case = "
			CASE 
				WHEN post_type = '" . esc_sql( $order_by ) . "' THEN 1
				WHEN post_type = 'page' THEN 2
				WHEN post_type = 'post' THEN 3
				WHEN post_type = 'product' THEN 4
			END
		";

		// Construir la consulta SQL final.
		$sql = "
			SELECT ID, post_title
			FROM {$table_name}
			WHERE post_type IN ($placeholders)
			AND post_status = %s
			ORDER BY {$order_case}, post_date DESC
			LIMIT %d OFFSET %d
		";

		// Preparar los parámetros.
		$params = array_merge( $post_types, array( 'publish', $items_per_page, $offset ) );

		// Preparar la consulta completa.

		// -------------------------------
		// Cache de 60 segundos
		// -------------------------------
		$cache_key      = 'compibot_sorted_' . md5( $query );
		$cache_group    = 'plugin_group';
		$cache_duration = 60;

		$cached_result = wp_cache_get( $cache_key, $cache_group );
		if ( false !== $cached_result ) {
			return $cached_result;
		}

		// Ejecutar la consulta.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
		$results = $wpdb->get_results( $wpdb->prepare( $sql, ...$params ) );

		// Cachear el resultado.
		wp_cache_set( $cache_key, $results, $cache_group, $cache_duration );

		// Formatear los resultados en un array.
		$items = array();
		foreach ( $results as $index => $row ) {
			$lang_info = apply_filters( 'wpml_post_language_details', null, $row->ID );// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
			$lang_code = isset( $lang_info['language_code'] ) ? $lang_info['language_code'] : 'es';

			$url = apply_filters( 'wpml_permalink', get_permalink( $row->ID ), $lang_code );// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound

			$items[] = array(
				'id'    => $index + 1 + $offset,
				'title' => $row->post_title,
				'url'   => $url,
			);
		}

		return $items;
	}



	/**
	 * Método para obtener la cantidad de publicaciones de un tipo específico con estado "publish".
	 *
	 * @param array $post_types Tipos de publicaciones a contar.
	 * @return int Número de publicaciones encontradas.
	 */
	public function compibot_count_posts_by_type( $post_types ) {
		// Validamos que $post_types es un array no vacío.
		if ( empty( $post_types ) || ! is_array( $post_types ) ) {
			return 0;
		}

		// Escapamos correctamente el nombre de la tabla.
		$table_name = esc_sql( $this->wpdb->prefix . 'posts' );

		// Creamos los placeholders dinámicos para post_type IN (...).
		$placeholders = implode( ',', array_fill( 0, count( $post_types ), '%s' ) );

		// Montamos la consulta SQL como string.
		$sql = "
			SELECT COUNT(*)
			FROM {$table_name}
			WHERE post_type IN ($placeholders)
			AND post_status = %s
		";

		// Creamos el array de parámetros.
		$params = array_merge( $post_types, array( 'publish' ) );

		// -------------------------------
		// Cache de 60 segundos
		// -------------------------------
		$cache_key      = 'compibot_count_' . md5( $sql );
		$cache_group    = 'plugin_group';
		$cache_duration = 60;

		$cached_result = wp_cache_get( $cache_key, $cache_group );
		if ( false !== $cached_result ) {
			return $cached_result;
		}

		$query = $this->wpdb->prepare(
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			"SELECT COUNT(*) FROM {$table_name} WHERE post_type IN ($placeholders) AND post_status = %s",
			...$params
		);

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared
		$count = (int) $this->wpdb->get_var( $query );

		// Guardar en caché.
		wp_cache_set( $cache_key, $count, $cache_group, $cache_duration );

		return $count;
	}


	/**
	 * Método para insertar datos en la tabla de Compibot.
	 *
	 * @param array      $data Datos a insertar en la tabla.
	 * @param array|null $format Formatos de los valores en $data.
	 * @return int|false ID de la fila insertada o false en caso de error.
	 */
	public function compibot_insert( $data, $format = null ) {
		// Validar que los datos no estén vacíos y sean un array.
		if ( empty( $data ) || ! is_array( $data ) ) {
			return false;
		}

		// Ejecutar la inserción en la base de datos.
		$result = $this->wpdb->insert(
			$this->table_name,
			$data,
			$format
		);

		return ( false !== $result ) ? $this->wpdb->insert_id : false;
	}


	/**
	 * Método que contiene la lógica para realizar consultas SELECT dependiendo de la columna especificada.
	 *
	 * @param string $column_name Nombre de la columna a consultar.
	 * @param string $cache_key Clave de la cache para la consulta (Por defecto vacio ya que se especifica en la funcion wp_cache_get).
	 * @param int    $cache_duration  Duración de la caché en segundos (Por defecto 60 segundos.).
	 * @return mixed Resultado de la consulta.
	 */
	public function compibot_select( $column_name, $cache_key = '', $cache_duration = 60 ) {
		if ( ! empty( $cache_key ) ) {
			$cached_result = wp_cache_get( $cache_key, 'plugin_group' );
			if ( false !== $cached_result ) {
				return $cached_result;
			}
		}

		$query = $this->wpdb->prepare(
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			"SELECT {$column_name} FROM {$this->table_name} WHERE id = %d",
			1
		);
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
		$result = $this->wpdb->get_var( $query ); 

		if ( ! empty( $cache_key ) ) {
			wp_cache_set( $cache_key, $result, 'plugin_group', $cache_duration );
		}

		return $result;
	}


	/**
	 * Método para realizar consultas SELECT que devuelvan más de 1 elemento en la respuesta.
	 *
	 * @param array|string $columns Array con los nombres de las columnas a consultas o '*' para todas las columnas.
	 * @param string       $cache_key Clave de la cache para la consulta.
	 * @param int          $cache_duration Duración de la caché en segundos(60 segundos por defecto).
	 * @return mixed Resultado de la consulta como un array asociativo o null si no hay resultados.
	 *
	 * @see https://www.php.net/implode Para más información sobre la función implode.
	 * @see https://www.php.net/manual/es/function.array-map.php Para más información sobre la función array_map.
	 */
	public function compibot_select_row( $columns = '*', $cache_key = '', $cache_duration = 60 ) {

		// Cacheamos la consulta.
		if ( ! empty( $cache_key ) ) {
			$cached_result = wp_cache_get( $cache_key, 'plugin_group' );
			if ( false !== $cached_result ) {
				return $cached_result;
			}
		}

		// Construimos la lista de columnas en el caso de que se obtenga un array.
		if ( is_array( $columns ) ) {
			// Se usa implode para convertir el array en un string separado por comas, se usa array_map para poder ejecutar "esc_sql" en cada elemento del array.
			$columns_sql = implode( ', ', array_map( 'esc_sql', $columns ) );
		} else {
			$columns_sql = '*' === $columns ? '*' : esc_sql( $columns );
		}

		// Preparamos y ejecutamos la consulta.
		$query = $this->wpdb->prepare(
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			"SELECT {$columns_sql} FROM {$this->table_name} WHERE id = %d",
			1
		);

		$result = $this->wpdb->get_row( $query, ARRAY_A ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		if ( ! empty( $cache_key ) ) {
			wp_cache_set( $cache_key, $result, 'plugin_group', $cache_duration );
		}

		return $result;
	}


	/**
	 * Método para obtener las columnas de la tabla actual (compibot)
	 *
	 * @return array Array con la lista de nombres de columnas de la tabla.
	 */
	public function compibot_get_columns() {
		$query = "SHOW COLUMNS FROM {$this->table_name}";
        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$columns = $this->wpdb->get_col( $query );

		return $columns;
	}




	/**
	 * Método para eliminar la tabla actual si existe
	 *
	 * Solo se llama en compibot.php para eliminar la tabla en caso de desactivar el plugin
	 *
	 * @return void
	 */
	public function compibot_drop_table() {
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$this->wpdb->query( $this->wpdb->prepare( "DROP TABLE IF EXISTS {$this->table_name}" ) );
	}


	/**
	 * Método para realizar actualizaciones en la tabla
	 *
	 * @param array      $data Datos a actualizar (columnas y sus valores).
	 * @param array      $where Condiciones para la actualización.
	 * @param array|null $format Formatos de los valores en $data.
	 * @param array|null $where_format Formatos de los valores en $where.
	 * @return int|false Número de filas afectadas o false en caso de error.
	 */
	public function compibot_update( $data, $where, $format = null, $where_format = null ) {
		$result = $this->wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.DirectQuery
			$this->table_name,
			$data,
			$where,
			$format,
			$where_format
		);

		return $result;
	}


	/**
	 * Método para ver si la tabla existe en la base de datos
	 *
	 * @return bool True si la tabla existe, false en caso contrario.
	 */
	public function compibot_table_exists() {
		$query  = "SHOW TABLES LIKE '{$this->table_name}'";
		$result = $this->wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared

		return $result === $this->table_name;
	}


	/**
	 * Método para actualizar la tabla basandonos en la estructura predefinida.
	 */
	public function update_table_schema() {

		$table_name = $this->table_name;

        // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$table_exists = $this->wpdb->get_var( $this->wpdb->prepare( 'SHOW TABLES LIKE %s', $table_name ) ) === $table_name;
		if ( ! $table_exists ) {
			return;
		}

		$columns = array(
			'id'            => 'mediumint(9) NOT NULL AUTO_INCREMENT',
			'token'         => "varchar(255) DEFAULT '0' NOT NULL",
			'public_token'  => "varchar(255) DEFAULT '0' NOT NULL",
			'active'        => "tinyint(1) DEFAULT '0' NOT NULL",
			'replace_flag'  => "tinyint(1) DEFAULT '0' NOT NULL",
			'last_scan'     => 'datetime DEFAULT NULL',
			'modulos'       => 'TEXT DEFAULT NULL',
			'site_type'     => 'varchar(20) DEFAULT NULL',
			'scanned_items' => "int(20) DEFAULT '0'",
			'scan_type'     => 'TEXT DEFAULT NULL',
			'kb_scanned'    => "float(10, 2) DEFAULT '0.00'",
			'kb_total'      => "float(10, 2) DEFAULT '0.00'",
			'kb_plan'       => "float(10, 2) DEFAULT '0.00'",
			'total_viewed'  => "int(20) DEFAULT '0'",
		);

		$existing_columns = array();
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared
		$result = $this->wpdb->get_results( "DESCRIBE $table_name" );
		foreach ( $result as $column ) {
			// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
			$existing_columns[ $column->Field ] = $column->Type;
		}

		$alter_queries = array();
		foreach ( $columns as $column_name => $column_definition ) {

			if ( ! preg_match( '/^[a-z0-9_]+$/i', $column_name ) ) {
				continue;
			}

			if ( ! array_key_exists( $column_name, $existing_columns ) ) {
				$alter_queries[] = "ADD COLUMN `$column_name` $column_definition";
			} elseif ( stripos( $existing_columns[ $column_name ], strtok( $column_definition, ' ' ) ) === false ) {
				$alter_queries[] = "MODIFY COLUMN `$column_name` $column_definition";
			}
		}
		if ( ! empty( $alter_queries ) ) {
			$alter_query = "ALTER TABLE `$table_name` " . implode( ', ', $alter_queries );
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter
			$this->wpdb->query( $alter_query );
		}
	}
}

// phpcs:enable
