<?php
declare( strict_types=1 );

namespace FiboFilters\Indexer;

use FiboFilters\Settings\Storage;
use FiboFilters\Util\Cache;

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

/**
 * Class BuildInfo
 */
class BuildInfo {

	const LAST_BUILD_OPTION_KEY = 'fibofilters_indexer_last_build';

	/** @var Cache */
	private $cache;

	/** @var Config */
	private $config;

	/** @var Storage */
	private $storage;

	/** @var bool */
	private $indexing = false;

	public function __construct(
		Cache $cache,
		Config $config,
		Storage $storage
	) {
		$this->config  = $config;
		$this->cache   = $cache;
		$this->storage = $storage;

		add_filter( 'fibofilters/build_info/is_indexing', [ $this, 'is_indexing' ] );
	}

	/**
	 * Add specific info about the last index build
	 *
	 * @param string $key Info key.
	 * @param mixed  $value Info value.
	 * @param string $index_role Index role.
	 */
	public function add_info( string $key, $value, $index_role = '' ): bool {
		// If empty, get "writable" index.
		if ( empty( $index_role ) ) {
			$index_role = $this->config->get_index_role();
		}
		$index_role_suffix = $index_role === 'main' ? '' : '_tmp';

		$added = update_option( self::LAST_BUILD_OPTION_KEY . '_' . $key . $index_role_suffix, $value );
		update_option( self::LAST_BUILD_OPTION_KEY . '_last_action_ts' . $index_role_suffix, time() );
		$this->cache->set( $key . '_' . $index_role, $value, 'indexer' );

		return $added;
	}

	/**
	 * Get index build info
	 *
	 * @param string $key Info key.
	 * @param string $index_role Index role.
	 *
	 * @return mixed|string
	 */
	public function get_info( string $key, string $index_role = 'main' ) {
		global $wpdb;

		if ( ! $this->is_indexing() ) {
			$value = $this->cache->get( $key . '_' . $index_role, 'indexer' );
			if ( $value !== false ) {
				return $value;
			}
		}

		$value = '';

		$index_role_suffix = $index_role === 'main' ? '' : '_tmp';

		if ( $key === 'logs' ) {
			$storage_key = 'indexer_last_build_logs' . $index_role_suffix;
			$logs_raw    = $this->storage->get( $storage_key, '', true );
			$value       = json_decode( $logs_raw, true );
			if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $value ) ) {
				$value = [];
			}
		} else {
			$info_key  = self::LAST_BUILD_OPTION_KEY . '_' . $key . $index_role_suffix;
			$raw_value = $wpdb->get_var( $wpdb->prepare( "SELECT SQL_NO_CACHE option_value FROM $wpdb->options WHERE option_name = %s", $info_key ) );

			if ( ! empty( $raw_value ) ) {
				$value = maybe_unserialize( $raw_value );
			}
		}

		$this->cache->set( $key . '_' . $index_role, $value, 'indexer' );

		return $value;
	}

	public function copy_indexer_last_build_logs_tmp_to_main() {
		$logs = $this->storage->get( 'indexer_last_build_logs_tmp', '', true );
		if ( empty( $logs ) ) {
			$logs = '[]';
		}
		$this->storage->update( 'indexer_last_build_logs', $logs, '' );
	}

	/**
	 * Structure of indexer data
	 */
	public function get_index_info_struct(): array {
		return [
			'build_id'                    => $this->generate_build_id_with_length_info(),
			'last_updated_date'           => '',
			'db'                          => 'MySQL',
			'status'                      => '',
			'start_ts'                    => time(),
			'start_filters_ts'            => 0,
			'end_ts'                      => 0,
			'end_filters_ts'              => 0,
			'last_action_ts'              => time(),
			'filters_processed'           => 0,
			'total_products_for_indexing' => 0,
			'simple_products_count'       => 0,
			'variable_products_count'     => 0,
			'variations_count'            => 0,
			'logs'                        => [],
			'non_critical_errors'         => [],
			'languages'                   => [],
			'plugin_version'              => '',
			'max_total_sales'             => 0,
		];
	}

	/**
	 * Two first digits of the build ID stores information about unique length of a special string,
	 * that will be displayed in <head> to prevent caches. The length are always different than in the previous build ID.
	 *
	 * Some cache systems might check only length of HTML, so we need display string with dynamic length reindexing.
	 * The two first digits are only information needed to generate the string with the unique-length
	 * in \FiboFilters\Indexer\DescriptorsCache::generate_dynamic_length_string().
	 *
	 * @return string
	 */
	private function generate_build_id_with_length_info(): string {

		$last_build = $this->get_info( 'build_id', 'main' );

		$length_info = 11;
		if ( ! empty( $last_build ) ) {
			$length_info = substr( $last_build, 0, 2 );
			if ( is_numeric( $length_info ) ) {
				$length_info = absint( $length_info );
				while ( ( $rand = rand( 11, 49 ) ) === $length_info ) {
				}
				$length_info = $rand;
			} else {
				$length_info = 11;
			}
		}

		return $length_info . substr( uniqid(), 2 );
	}

	public function create_info_struct(): void {
		$index_role_suffix = $this->config->get_index_role() === 'main' ? '' : '_tmp';
		foreach ( $this->get_index_info_struct() as $key => $value ) {
			if ( $key === 'logs' ) {
				$this->storage->update( 'indexer_last_build_logs' . $index_role_suffix, '[]' );
				continue;
			}
			update_option( self::LAST_BUILD_OPTION_KEY . '_' . $key . $index_role_suffix, $value, 'no' );
		}
	}

	/**
	 * Get last index build info
	 *
	 * @param string $index_role_suffix Index role suffix.
	 *
	 * @return array
	 */
	public function get_last_build_info( string $index_role_suffix = '' ): array {
		global $wpdb;

		$data = [];

		$opt = $wpdb->get_var( $wpdb->prepare( "SELECT SQL_NO_CACHE option_value FROM $wpdb->options WHERE option_name = %s", self::LAST_BUILD_OPTION_KEY . $index_role_suffix ) );

		if ( ! empty( $opt ) ) {
			$opt = @unserialize( $opt );
			if ( is_array( $opt ) ) {
				$data = $opt;
			}
		}

		return $data;
	}

	public function add_log( string $message, string $level = 'info', string $index_role = '' ): void {
		if ( empty( $index_role ) ) {
			$index_role = $this->config->get_index_role();
		}

		$index_role_suffix = $index_role === 'main' ? '' : '_tmp';
		$storage_key       = 'indexer_last_build_logs' . $index_role_suffix;

		$logs_raw = $this->storage->get( $storage_key, '', true );
		if ( $logs_raw === '' ) {
			$logs_raw = '[]';
		}
		$logs = json_decode( $logs_raw, true );
		if ( json_last_error() !== JSON_ERROR_NONE || ! is_array( $logs ) ) {
			$logs = [];
		}

		$logs[] = [
			'time'    => time(),
			'error'   => in_array( $level, [ 'emergency', 'warning', 'notice' ], true ),
			'message' => $message,
		];

		$this->storage->update( $storage_key, wp_json_encode( $logs ) );
	}

	/**
	 * Check if the indexer working too long without any action
	 *
	 * @param int $force_max_no_action_time Max no action time to test.
	 *
	 * @return bool
	 */
	public function is_indexer_working_too_long( int $force_max_no_action_time = 0 ): bool {
		$status = $this->get_info( 'status', $this->config->get_index_role() );

		// Return early if indexer is not working.
		if ( ! in_array( $status, [ 'building', 'preparing' ], true ) ) {
			return false;
		}

		$last_action_ts = absint( $this->get_info( 'last_action_ts', $this->config->get_index_role() ) );

		// Return early if the indexer info hasn't been created yet.
		if ( empty( $last_action_ts ) ) {
			return false;
		}

		$diff = time() - $last_action_ts;

		$max_no_action_time = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ? 61 * MINUTE_IN_SECONDS : 16 * MINUTE_IN_SECONDS;

		/**
		 * Filters maximum no action time of indexer.
		 *
		 * @param int $max_no_action_time Max time in seconds. 16 min if WP-Cron is enabled or 61 min if not
		 */
		$max_no_action_time = apply_filters( 'fibofilters/indexer/max_no_action_time', $max_no_action_time );

		if ( $force_max_no_action_time > 0 ) {
			$max_no_action_time = $force_max_no_action_time;
		}

		return $diff >= $max_no_action_time;
	}

	/**
	 * @return bool
	 */
	public function is_indexing(): bool {
		return $this->indexing;
	}

	/**
	 * Set indexing status
	 *
	 * @param bool $indexing Indexing status.
	 */
	public function set_is_indexing( bool $indexing ): void {
		$this->indexing = $indexing;
	}
}
