<?php

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

/**
 * Class RIO_Queue_Model is a database communication model for "prefix_rio_process_queue" table.
 *
 * @property int|null                 $id                     Primary key.
 * @property null|string              $server_id              Server id which was used for processing.
 * @property null|int                 $object_id              Object id. Usually, this would be foreign key to another table.
 * @property null|string              $object_name            Object name. Usually, this can be table name where to take object id.
 * @property null|string              $item_type              Type of item. E.g. attachment, webp, etc.
 * @property null|string              $item_hash              Unique item hash. Serves purpose of search.
 * @property null|string              $item_hash_alternative  Unique alternative hash. Serves purpose of search.
 * @property null|string              $result_status          Status result.
 * @property null|string              $processing_level
 * @property null|bool                $is_backed_up           Whether item is backed-up or not. = false;
 * @property null|int                 $original_size          Original file size in bytes.
 * @property null|int                 $final_size             Final file size in bytes after it was optimized or converted.
 * @property null|string              $original_mime_type     Original MIME TYPE. e.g. image/jpeg.
 * @property null|string              $final_mime_type        Final mime type. e.g. image/webp.
 * @property null|RIO_Base_Extra_Data $extra_data             Extra data to be saved. e.g. JSON response from API.
 * @property null|int                 $created_at             UNIX timestamp when item was saved.
 */
class RIO_Process_Queue extends RIO_Base_Active_Record {

	/**
	 * When new status is added, it should be added accordingly to get_statuses() method.
	 *
	 * @see get_statuses() for further information.
	 */
	const STATUS_SUCCESS     = 'success';         // On success to convert or optimize.
	const STATUS_ERROR       = 'error';             // On failure to convert or optimize.
	const STATUS_SKIP        = 'skip';               // Skip.
	const STATUS_PROCESSING  = 'processing';   // When conversion or optimization is in progress.
	const STATUS_UNOPTIMIZED = 'unoptimized'; // Когда картинка не оптимизирована

	/**
	 * When new level is added, it should be added accordingly to get_levels() method.
	 *
	 * @see get_levels() for further information.
	 */
	const LEVEL_NORMAL    = 'normal';
	const LEVEL_AGGRESIVE = 'aggresive'; // todo: need to fix typo in word aggressive
	const LEVEL_ULTRA     = 'ultra';
	const LEVEL_CUSTOM    = 'custom';

	/**
	 * @var null|int Primary key.
	 */
	protected $id = null;

	/**
	 * @var null|string Server id which was used for processing.
	 */
	protected $server_id = null;

	/**
	 * @var null|int Object id. Usually, this would be foreign key to another table.
	 */
	protected $object_id = null;

	/**
	 * @var null|string Object name. Usually, this can be table name where to take object id.
	 */
	protected $object_name = null;

	/**
	 * @var null|string Type of item. E.g. attachment, webp, etc.
	 */
	protected $item_type = null;

	/**
	 * @var null|string Unique item hash. Serves purpose of search.
	 */
	protected $item_hash = null;

	/**
	 * @var null|string Unique alternative hash. Serves purpose of search.
	 */
	protected $item_hash_alternative = null;

	/**
	 * @var null|string Status result.
	 */
	protected $result_status = null;

	/**
	 * @var null|string
	 */
	protected $processing_level = null;

	/**
	 * @var null|bool Whether item is backed-up or not.
	 */
	protected $is_backed_up = false;

	/**
	 * @var null|int Original file size in bytes.
	 */
	protected $original_size = null;

	/**
	 * @var null|int Final file size in bytes after it was optimized or converted.
	 */
	protected $final_size = null;

	/**
	 * @var null|string Original MIME TYPE. e.g. image/jpeg.
	 */
	protected $original_mime_type = null;

	/**
	 * @var null|string Final mime type. e.g. image/webp.
	 */
	protected $final_mime_type = null;

	/**
	 * @var null|RIO_Base_Extra_Data Extra data to be saved. e.g. JSON response from API.
	 */
	protected $extra_data = null;

	/**
	 * @var null|int UNIX timestamp when item was saved.
	 */
	protected $created_at = null;

	/**
	 * @var RIO_Base_Extra_Data Keeps instance of extra data to leave state of $extra_data untouched.
	 */
	private static $_extra_data;

	/**
	 * {@inheritdoc}
	 */
	public function init() {
		parent::init();

		// Support both webp and avif with unified extra data model
		if ( in_array( $this->item_type, [ 'webp', 'avif' ] ) ) {
			static::$_extra_data = new RIOP_WebP_Extra_Data(); // Works for both formats
		} else {
			static::$_extra_data = new RIO_Base_Extra_Data();
		}
	}

	/**
	 * {@inheritdoc}
	 */
	public static function table_name() {
		global $wpdb;

		return $wpdb->prefix . 'rio_process_queue';
	}

	/**
	 * It is used to check the migration and create a schema in the database.
	 * Gets the level of activity performed in the database.
	 *
	 * @return int
	 * @since  1.3.8
	 */
	public static function get_db_version() {
		if ( WRIO_Plugin::app()->isNetworkActive() ) {
			return (int) get_site_option( WRIO_Plugin::app()->getOptionName( 'db_version' ), 0 );
		}

		return (int) get_option( WRIO_Plugin::app()->getOptionName( 'db_version' ), 0 );
	}

	/**
	 * It is used to check the migration and create a schema in the database.
	 * Updates the level of activity performed in the database.
	 *
	 * @return void
	 * @since  1.3.8
	 */
	public static function update_db_version( $version ) {
		if ( WRIO_Plugin::app()->isNetworkActive() ) {
			update_site_option( WRIO_Plugin::app()->getOptionName( 'db_version' ), (int) $version );

			return;
		}

		update_option( WRIO_Plugin::app()->getOptionName( 'db_version' ), (int) $version );
	}

	/**
	 * Find db item by hash.
	 *
	 * @param string $hash Hash to search for.
	 *
	 * @return null|$this
	 */
	public static function find_by_hash( $hash ) {
		global $wpdb;

		if ( empty( $hash ) ) {
			return null;
		}

		$table_name = static::table_name();
		$sql        = "SELECT * FROM {$table_name} WHERE `item_hash` = %s";

		$row = $wpdb->get_row( $wpdb->prepare( $sql, $hash ), ARRAY_A );

		if ( empty( $row ) ) {
			return null;
		}

		return new RIO_Process_Queue( $row );
	}

	/**
	 * Find all by specified condition.
	 *
	 * @param array  $condition Key => value condition to be used to search.
	 * @param string $order
	 * @param string $limit
	 *
	 * @return null|self[]
	 */
	public static function find_all( array $condition ) {

		global $wpdb;

		$table = static::table_name();

		$sql = "SELECT * FROM $table WHERE ";

		foreach ( $condition as $key => $value ) {
			if ( is_numeric( $value ) ) {
				$sql .= ' `' . esc_sql( $key ) . "` = $value AND";
			} elseif ( is_string( $value ) ) {
				$sql .= ' `' . esc_sql( $key ) . "` = '" . esc_sql( $value ) . "' AND";
			}
		}

		$sql = rtrim( $sql, 'AND' );

		$rows = $wpdb->get_results( $sql, ARRAY_A );

		if ( empty( $rows ) ) {
			return null;
		}

		foreach ( $rows as $key => $row ) {
			$rows[ $key ] = new RIO_Process_Queue( $row );
		}

		return $rows;
	}

	/**
	 * Find items by list of hashes.
	 *
	 * @param string|array $hashes List of hashes in form of array or CSV.
	 * @param string|null  $status
	 *
	 * @return null|RIO_Process_Queue[]
	 */
	public static function find_by_hashes( $hashes, $status = null ) {
		if ( ! is_array( $hashes ) && ! is_string( $hashes ) ) {
			return null;
		}

		if ( ! is_array( $hashes ) && false !== strpos( $hashes, ',' ) ) {
			$hashes = explode( ',', $hashes );
		}

		$hashes = array_map( 'trim', $hashes );

		$table_name = static::table_name();
		$sql        = "SELECT * FROM {$table_name} WHERE `item_hash`";

		$hashes = array_map(
			function ( $hash ) {
				return "'$hash'";
			},
			$hashes
		);

		$sql .= ' IN (' . implode( ', ', $hashes ) . ')';

		global $wpdb;

		if ( $status !== null ) {
			$sql .= $wpdb->prepare( ' AND `result_status` = %s', $status );
		}

		$rows = $wpdb->get_results( $sql, ARRAY_A );

		if ( empty( $rows ) ) {
			return null;
		}

		foreach ( $rows as $key => $row ) {
			$rows[ $key ] = new RIO_Process_Queue( $row );
		}

		return $rows;
	}

	/**
	 * Find db item by alternative hash.
	 *
	 * @param string $hash Hash to search for.
	 *
	 * @return null|$this
	 */
	public static function find_by_alternative_hash( $hash ) {
		global $wpdb;

		$table_name = static::table_name();
		$sql        = "SELECT * FROM {$table_name} WHERE `item_alternative_hash` = %s";

		$row = $wpdb->get_row( $wpdb->prepare( $sql, $hash ), ARRAY_A );

		if ( empty( $row ) ) {
			return null;
		}

		return new RIO_Process_Queue( $row );
	}

	/**
	 * Get next item to process sorted by ASC (first inserted).
	 *
	 * @param string   $type      Type to search for.
	 * @param int|null $object_id Object id added to the query.
	 * @param int|null $limit     Limit added to the query.
	 *
	 * @return null|RIO_Process_Queue[]
	 */
	public static function find_next_to_process( $type, $object_id = null, $limit = null ) {
		global $wpdb;

		$table_name = static::table_name();

		$where = $wpdb->prepare( "WHERE `item_type`= %s AND `result_status` = 'processing'", $type );

		if ( ! empty( $object_id ) ) {
			$where .= sprintf( ' AND `object_id` = %d', $object_id );
		}

		$sql = "SELECT * FROM {$table_name} {$where} ORDER BY `id` ASC";

		if ( is_int( $limit ) ) {
			$sql .= ' LIMIT ' . $limit;
		}

		$rows = $wpdb->get_results( $sql, ARRAY_A );

		if ( empty( $rows ) ) {
			return null;
		}

		foreach ( $rows as $key => $row ) {
			$rows[ $key ] = new RIO_Process_Queue( $row );
		}

		return $rows;
	}

	/**
	 * Get count var by specified status.
	 *
	 * Notice: when type is undefined, it would return false.
	 *
	 * @param string $status Status conversion or optimization.
	 *
	 * @return bool
	 */
	public static function count_by_status( $status ) {
		if ( empty( $status ) ) {
			return false;
		}

		if ( ! in_array( $status, static::get_statuses() ) ) {
			return false;
		}

		global $wpdb;

		$table_name   = static::table_name();
		$sql          = "SELECT COUNT(*) FROM {$table_name} WHERE `result_status` = %s";
		$prepared_sql = $wpdb->prepare( $sql, [ $status ] );

		return $wpdb->get_var( $prepared_sql );
	}

	/**
	 * Get count var by specified type and status.
	 *
	 * Notice: when type is undefined, it would return false.
	 *
	 * @param string $type   Type of file. Normally defined by TYPE_* constant.
	 * @param string $status Status conversion or optimization.
	 *
	 * @return false|int|string|null
	 */
	public static function count_by_type_status( $type, $status ) {

		if ( empty( $type ) || empty( $status ) ) {
			return false;
		}

		if ( ! in_array( $status, static::get_statuses() ) ) {
			return false;
		}

		global $wpdb;

		$table_name = static::table_name();
		if ( $type == 'webp' ) {
			$sql = "SELECT DISTINCT object_id FROM {$table_name} WHERE `item_type` = %s AND `result_status` = %s";
		} else {
			$sql = "SELECT COUNT(*) FROM {$table_name} WHERE `item_type` = %s AND `result_status` = %s";
		}

		$prepared_sql = $wpdb->prepare( $sql, [ $type, $status ] );

		if ( $type == 'webp' ) {
			$result = $wpdb->get_results( $prepared_sql );

			return is_countable( $result ) ? count( $result ) : 0;
		} else {
			return $wpdb->get_var( $prepared_sql );
		}
	}

	/**
	 * Get count value for specified type, status and level.
	 *
	 * @param string $type   Type of file. Normally defined by TYPE_* constant.
	 * @param string $status Status conversion or optimization.
	 * @param string $level  Level of optimization or conversion.
	 *
	 * @return bool|null|string
	 */
	public static function count_by_type_status_level( $type, $status, $level ) {
		if ( empty( $type ) || empty( $status ) || empty( $level ) ) {
			return false;
		}

		if ( ! in_array( $status, static::get_statuses() ) ) {
			return false;
		}

		if ( ! in_array( $level, static::get_levels() ) ) {
			return false;
		}

		global $wpdb;

		$table_name   = static::table_name();
		$sql          = "SELECT COUNT(*) FROM {$table_name} WHERE `item_type` = %s AND `result_status` = %s AND `processing_level` = %s";
		$prepared_sql = $wpdb->prepare( $sql, [ $type, $status, $level ] );

		return $wpdb->get_var( $prepared_sql );
	}

	/**
	 * Get available types.
	 *
	 * @return array
	 */
	public static function get_statuses() {
		return [
			self::STATUS_SUCCESS,
			self::STATUS_ERROR,
			self::STATUS_PROCESSING,
			self::STATUS_UNOPTIMIZED,
			self::STATUS_SKIP,
		];
	}

	/**
	 * Get available levels.
	 *
	 * @return array
	 */
	public static function get_levels() {
		return [ self::LEVEL_NORMAL, self::LEVEL_AGGRESIVE, self::LEVEL_CUSTOM, self::LEVEL_ULTRA ];
	}

	/**
	 * Обновление свойств объекта из базы данных
	 *
	 * @return void
	 */
	public function load() {
		global $wpdb;
		$table_name = static::table_name();
		$sql        = $wpdb->prepare(
			"SELECT * FROM {$table_name} WHERE object_id = %d AND item_type = %s LIMIT 1;",
			[
				$this->object_id,
				$this->item_type,
			]
		);

		$row = $wpdb->get_row( $sql );

		if ( ! empty( $row ) ) {
			$this->configure( $row );
		}
	}

	/**
	 * Проверяет, оптимизированно ли изображение
	 *
	 * @return bool
	 */
	public function is_optimized() {
		if ( $this->result_status == self::STATUS_SUCCESS ) {
			return true;
		}

		return false;
	}

	/**
	 * Проверяет, пропущено ли изображение
	 * Если изображение пропущено, то оно больше не участвует в оптимизации
	 *
	 * @return bool
	 */
	public function is_skipped() {
		if ( $this->result_status == self::STATUS_SKIP ) {
			return true;
		}

		return false;
	}

	/**
	 * Подготовка данных для сохранения в базе
	 *
	 * @return array свойства модели в виде массива. Подготовлены для сохранения в базе данных
	 */
	public function prepare_data_to_save() {
		if ( ! $this->created_at ) {
			$this->created_at = time();
		}

		if ( $this->extra_data instanceof RIO_Base_Extra_Data ) {
			$this->extra_data = (string) $this->extra_data;
		}

		$data = (array) $this;

		// @todo: remove later to usage of private attributes inside model
		foreach ( $data as $key => $value ) {
			$clean_key          = trim( $key, " \t\n\r\0\x0B*" );
			$data[ $clean_key ] = $value;
			unset( $data[ $key ] );
		}

		unset( $data['id'] );

		return $data;
	}

	/**
	 * Save item.
	 *
	 * @return bool
	 */
	public function save() {
		global $wpdb;
		$table_name = static::table_name();
		$data       = $this->prepare_data_to_save();

		$is_success = false;

		// если установлен id - значит данные уже есть в базе
		if ( $this->id ) {

			// если данные есть в базе, то обновляем их
			$result = $wpdb->update( $table_name, $data, [ 'id' => $this->id ] );

			WRIO_Plugin::app()->logger->debug( sprintf( 'Updated queue item #%s, attributes values: %s', $this->id, wp_json_encode( $data ) ) );

			$is_success = true;
		} elseif ( ! self::find_by_hash( $this->get_item_hash() ) ) {
				// если данных нет в базе, то вставляем новую запись в таблицу
				$count = $wpdb->insert( $table_name, $data );

			if ( $count !== false && $count > 0 ) {
				$lastId = $wpdb->insert_id;

				if ( ! empty( $lastId ) ) {
					$this->id = $lastId;

					$is_success = true;

					WRIO_Plugin::app()->logger->debug( sprintf( 'New queue created #%s, attributes values: %s', $this->id, wp_json_encode( $data ) ) );
				}
			}
		}

		/**
		 * Filter whether to execute hook or not.
		 *
		 * @param $execute_hook bool
		 */
		$execute_hook = apply_filters( 'wbcr/riop/queue_item_save_execute_hook', true );

		if ( $is_success && $execute_hook ) {
			/**
			 * Fires after queue item was saved or updated successfully.
			 *
			 * @param RIO_Process_Queue $this
			 * @param bool              $quota Deduct from the quota?
			 */
			do_action( 'wbcr/riop/queue_item_saved', $this, false );
		}

		return $is_success;
	}

	/**
	 * Удаление информации об оптимизации из таблицы в базе данных
	 *
	 * @return bool
	 */
	public function delete() {
		global $wpdb;
		$db_table = static::table_name();

		$rows_affected = $wpdb->delete( $db_table, [ 'id' => $this->id ] );

		WRIO_Plugin::app()->logger->debug( sprintf( 'Deleted queue item #%s', $this->id ) );

		return $rows_affected !== false;
	}

	/**
	 * Возвращает extra_data
	 *
	 * @return RIO_Base_Extra_Data|null
	 */
	public function get_extra_data() {

		if ( empty( $this->extra_data ) ) {
			return null;
		}

		if ( is_string( $this->extra_data ) ) {
			$extra_data = (array) json_decode( $this->extra_data );

			if ( ! empty( $extra_data ) ) {
				$class = isset( $extra_data['class'] ) ? $extra_data['class'] : null;

				if ( ! empty( $class ) && class_exists( $class ) ) {
					$this->extra_data = new $class( $extra_data );

					return $this->extra_data;
				}
			}
		}

		if ( is_object( $this->extra_data ) ) {
			return $this->extra_data;
		}

		return null;
	}

	/**
	 * Устанавливает значение для массива метаданных
	 *
	 * @param object|string $extra_data
	 *
	 * @return void
	 */
	public function set_extra_data( $extra_data ) {
		$this->extra_data = $extra_data;
	}

	/**
	 * Mark the current queue item as failed and persist its error details.
	 *
	 * @param string               $error_message Failure message to persist.
	 * @param array<string, mixed> $attributes    Additional queue attributes to overwrite before saving.
	 *
	 * @return bool
	 */
	public function mark_as_error( $error_message, $attributes = [] ) {
		$extra_data = $this->get_extra_data();

		if ( ! $extra_data instanceof RIO_Attachment_Extra_Data ) {
			$extra_data = in_array( $this->get_item_type(), [ 'webp', 'avif' ], true )
				? new RIOP_WebP_Extra_Data()
				: new RIO_Attachment_Extra_Data();
		}

		$extra_data->set_error( 'exception' );
		$extra_data->set_error_msg( $error_message );

		$this->set_result_status( self::STATUS_ERROR );

		if ( ! empty( $attributes ) ) {
			$this->configure( $attributes );
		}

		$this->set_extra_data( $extra_data );

		return $this->save();
	}

	/**
	 * Set item hash.
	 *
	 * @param string $text String to be hashed.
	 */
	public function set_item_hash( $text ) {
		if ( ! empty( $text ) ) {
			$this->item_hash = static::generate_item_hash( $text );
		}
	}

	/**
	 * @param int|null $id
	 */
	public function set_id( $id ) {
		if ( is_numeric( $id ) ) {
			$this->id = (int) $id;
		}
	}

	/**
	 * @return int|null
	 */
	public function get_id() {

		if ( is_numeric( $this->id ) ) {
			return (int) $this->id;
		}

		return $this->id;
	}

	/**
	 * @param null|string $server_id
	 */
	public function set_server_id( $server_id ) {
		$this->server_id = $server_id;
	}

	public function get_server_id() {
		return $this->server_id;
	}

	/**
	 * @param int|null $object_id
	 */
	public function set_object_id( $object_id ) {
		$this->object_id = $object_id;
	}

	public function get_object_id() {
		return $this->object_id;
	}

	/**
	 * @param null|string $object_name
	 */
	public function set_object_name( $object_name ) {
		$this->object_name = $object_name;
	}

	public function get_object_name() {
		return $this->object_name;
	}

	/**
	 * @param null|string $item_type
	 */
	public function set_item_type( $item_type ) {
		$this->item_type = $item_type;
	}

	public function get_item_type() {
		return $this->item_type;
	}

	/**
	 * @param null|string $text Text to be hashed.
	 */
	public function set_item_hash_alternative( $text ) {
		if ( ! empty( $text ) ) {
			$this->item_hash_alternative = static::generate_item_alternative_hash( $text );
		}
	}

	public function get_item_hash_alternative() {
		return $this->item_hash_alternative;
	}

	/**
	 * @return null|string
	 */
	public function get_item_hash() {
		return $this->item_hash;
	}

	/**
	 * @param null|string $result_status
	 */
	public function set_result_status( $result_status ) {
		$this->result_status = $result_status;
	}

	public function get_result_status() {
		return $this->result_status;
	}

	/**
	 * @param null|string $processing_level
	 */
	public function set_processing_level( $processing_level ) {
		$this->processing_level = $processing_level;
	}

	public function get_processing_level() {
		return $this->processing_level;
	}

	/**
	 * @param bool|null $is_backed_up
	 */
	public function set_is_backed_up( $is_backed_up ) {
		$this->is_backed_up = $is_backed_up;
	}

	public function get_is_backed_up() {
		return $this->is_backed_up;
	}

	/**
	 * @param int|null $original_size
	 */
	public function set_original_size( $original_size ) {
		$this->original_size = $original_size;
	}

	public function get_original_size() {
		return $this->original_size;
	}

	/**
	 * @param int|null $final_size
	 */
	public function set_final_size( $final_size ) {
		$this->final_size = $final_size;
	}

	public function get_final_size() {
		return $this->final_size;
	}

	/**
	 * @param null|string $original_mime_type
	 */
	public function set_original_mime_type( $original_mime_type ) {
		$this->original_mime_type = $original_mime_type;
	}

	public function get_original_mime_type() {
		return $this->original_mime_type;
	}

	/**
	 * @param null|string $final_mime_type
	 */
	public function set_final_mime_type( $final_mime_type ) {
		$this->final_mime_type = $final_mime_type;
	}

	public function get_final_mime_type() {
		return $this->final_mime_type;
	}

	/**
	 * @param int|null $created_at
	 */
	public function set_created_at( $created_at ) {
		$this->created_at = $created_at;
	}

	/**
	 * get_created_at
	 *
	 * @return int
	 */
	public function get_created_at() {
		return $this->created_at;
	}

	/**
	 * Generate item hash.
	 *
	 * @param string $text Text to be hashed.
	 *
	 * @return string
	 */
	public static function generate_item_hash( $text ) {
		return hash( 'sha256', $text );
	}

	/**
	 * Generate item alternative hash.
	 *
	 * @param string $text Text to be hashed.
	 *
	 * @return string
	 */
	public static function generate_item_alternative_hash( $text ) {
		return hash( 'sha256', $text );
	}

	/**
	 * Get table schema.
	 *
	 * @return string
	 */
	public static function get_table_schema() {
		global $wpdb;
		$charset_collate = $wpdb->get_charset_collate();
		$table_name      = self::table_name();
		$sql             = "CREATE TABLE IF NOT EXISTS {$table_name} (
			  `id` bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
			  `server_id` varchar(60) DEFAULT NULL,
			  `object_id` bigint(20) UNSIGNED NULL,
			  `object_name` varchar(255) NULL,
			  `item_type` varchar(60) NOT NULL,
			  `item_hash` CHAR(64) NULL COMMENT 'sha256 size',
			  `item_hash_alternative` CHAR(64) NULL COMMENT 'sha256 size',
			  `result_status` varchar(60) NOT NULL,
			  `processing_level` varchar(60) NOT NULL,
			  `is_backed_up` tinyint(1) NOT NULL DEFAULT '0',
			  `original_size` int(11) UNSIGNED NOT NULL,
			  `final_size` int(11) UNSIGNED NOT NULL,
			  `original_mime_type` varchar(60) NOT NULL,
			  `final_mime_type` varchar(60) NOT NULL,
			  `extra_data` TEXT NULL DEFAULT NULL,
			  `created_at` bigint(20) NOT NULL,
			  PRIMARY KEY (`id`)
			) $charset_collate;";

		return $sql;
	}

	/**
	 * {@inheritdoc}
	 */
	public static function get_table_indexes() {
		$table_name                  = static::table_name();
		$sql_index_type_status       = "ALTER TABLE {$table_name} ADD INDEX `index-type-status` (`item_type`, `result_status`);";
		$sql_index_type_status_level = "ALTER TABLE {$table_name} ADD INDEX `index-type-status-level` (`item_type`, `result_status`, `processing_level`);";
		$sql_index_hash              = "ALTER TABLE {$table_name} ADD UNIQUE `index-hash` (`item_hash`);";
		$sql_index_hash_alternative  = "ALTER TABLE {$table_name} ADD INDEX `index-hash-alternative` (`item_hash_alternative`);";
		$sql_index_type_attachments  = "ALTER TABLE {$table_name} ADD INDEX `index-type-attachments` (`object_id`, `item_type`);";

		return [
			$sql_index_type_status,
			$sql_index_type_status_level,
			$sql_index_hash,
			$sql_index_hash_alternative,
			$sql_index_type_attachments,
		];
	}

	/**
	 * Try to create a plugin table in database
	 *
	 * @throws \Exception
	 * @since  1.3.6
	 */
	public static function try_create_plugin_tables() {
		global $wpdb;

		try {
			if ( ! self::has_table_schema() ) {
				return;
			}

			if ( ! static::get_db_version() ) {
				$sql = static::get_table_schema();
				$wpdb->query( $sql );

				if ( static::has_table_indexes() ) {
					$indexes = static::get_table_indexes();

					foreach ( $indexes as $index ) {
						$wpdb->query( $index );
					}
				}

				static::update_db_version( 1 );
				static::fix_table_collation();
			}
		} catch ( \Exception $e ) {
			WRIO_Plugin::app()->logger->error( sprintf( "Failed create %s table in database.\r\nSQL: %s", static::table_name(), static::get_table_schema() ) );
		}
	}

	/**
	 * RIO-126: Fix collation for plugin table, if WordPress tables are created in a different encoding.
	 * In some cases, plugin table can be utf8mb4_unicode_520_ci, and Wodpress table can be
	 * utf8mb4_unicode_ci. This leads to problems when performing comparisons between two tables.
	 *
	 * @since  1.3.6
	 */
	public static function fix_table_collation() {
		global $wpdb;

		$wp_post_meta_collation  = null;
		$process_queue_collation = null;
		$wrio_table              = static::table_name();

		$result = $wpdb->get_results( 'SHOW TABLE STATUS' );

		if ( ! empty( $result ) ) {
			foreach ( (array) $result as $table ) {
				if ( $wpdb->postmeta === $table->Name ) {
					$wp_post_meta_collation = $table->Collation;
				} elseif ( $wpdb->prefix . 'rio_process_queue' === $table->Name ) {
					$process_queue_collation = $table->Collation;
				}
			}

			if ( ! empty( $wp_post_meta_collation ) && ! empty( $process_queue_collation ) ) {

				[$wp_post_meta_charset]  = explode( '_', $wp_post_meta_collation );
				[$process_queue_charset] = explode( '_', $process_queue_collation );

				if ( ( $wp_post_meta_collation !== $process_queue_collation ) && $wp_post_meta_charset === $process_queue_charset ) {
					$wpdb->query( "ALTER TABLE {$wrio_table} CONVERT TO CHARACTER SET {$wp_post_meta_charset} COLLATE {$wp_post_meta_collation}" );

					WRIO_Plugin::app()->logger->info( sprintf( "Successfully fix collation for plugin table.\r\nWP COLLATION: %s | PLUGIN COLLATION: %s", $wp_post_meta_collation, $process_queue_collation ) );
				}
			}
		}
	}
}
