<?php
declare( strict_types=1 );

namespace FiboFilters\Indexer;

use FiboFilters\Config\Env;
use FiboFilters\Filters\StoreFilters;
use FiboFilters\Indexer\Filters\AsyncProcess;
use FiboFilters\Indexer\Filters\Database as DatabaseFilters;
use FiboFilters\Indexer\Filters\Indexer as IndexerFilters;
use FiboFilters\Integrations\Multilingual;
use FiboFilters\Util\DbUtil;
use FiboFilters\WooCommerce\Helper as WooCommerceHelper;
use FiboFilters\WP\Helper as WPHelper;

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

/**
 * Class Builder
 */
class Builder {
	const LAST_BUILD_OPTION_KEY              = 'fibofilters_indexer_last_build';
	const INDEXING_PREPARE_PROCESS_EXIST_KEY = 'fibofilters_indexer_prepare_process_exist';
	const FILTERS_SET_ITEMS_COUNT            = 50;

	/** @var AsyncProcess */
	private $async_process;

	/** @var BuildInfo */
	private $build_info;

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

	/** @var DatabaseFilters */
	private $database_filters;

	/** @var DescriptorsCache */
	private $descriptors_cache;

	/** @var DbUtil */
	private $db_util;

	/** @var IndexerFilters */
	private $indexer_filters;

	/** @var Logger */
	private $logger;

	/** @var Multilingual */
	private $multilingual;

	/** @var SourceQueryFactory */
	private $source_query_factory;

	/** @var StoreFilters */
	private $store_filters;

	/** @var WooCommerceHelper */
	private $woocommerce_helper;

	/** @var WPHelper */
	private $wp_helper;

	public function __construct(
		AsyncProcess $async_process,
		BuildInfo $build_info,
		Config $config,
		DatabaseFilters $database_filters,
		DescriptorsCache $descriptors_cache,
		DbUtil $db_util,
		IndexerFilters $indexer_filters,
		Logger $logger,
		Multilingual $multilingual,
		SourceQueryFactory $source_query_factory,
		StoreFilters $store_filters,
		WooCommerceHelper $woocommerce_helper,
		WPHelper $wp_helper
	) {
		$this->async_process        = $async_process;
		$this->build_info           = $build_info;
		$this->config               = $config;
		$this->database_filters     = $database_filters;
		$this->descriptors_cache    = $descriptors_cache;
		$this->db_util              = $db_util;
		$this->indexer_filters      = $indexer_filters;
		$this->logger               = $logger;
		$this->multilingual         = $multilingual;
		$this->source_query_factory = $source_query_factory;
		$this->store_filters        = $store_filters;
		$this->woocommerce_helper   = $woocommerce_helper;
		$this->wp_helper            = $wp_helper;
	}

	public function init(): void {
		add_action( 'fibofilters/indexer/maybe_completed', [ $this, 'maybe_completed' ] );
		add_action( 'fibofilters/indexer/status/started', [ $this->woocommerce_helper , 'regenerate_attributes_lookup_sync' ] );

		$this->async_process->init_handler();
		$this->async_process->init_cron();
	}

	public function prepare_build_index(): bool {
		global $wpdb;

		$this->build_info->set_is_indexing( true );

		if ( $this->wp_helper->is_lock_locked( self::INDEXING_PREPARE_PROCESS_EXIST_KEY, 5 ) ) {
			$this->logger->log( 'Indexer already preparing building the index' );

			return false;
		}

		$this->logger->remove_logs();

		if ( ! $this->config->is_indexer_mode( 'direct' ) ) {
			$this->wp_helper->set_lock( self::INDEXING_PREPARE_PROCESS_EXIST_KEY );
		}

		$this->build_info->create_info_struct();

		$this->logger->log( sprintf( 'Indexer mode: %s', $this->config->get_indexer_mode() ) );
		$this->logger->log( sprintf( 'Parallel build: %s', $this->config->is_parallel_building_enabled() ? 'yes' : 'no' ) );

		$this->cancel_build_index( '', '', false );

		$this->build_info->add_info( 'status', 'preparing' );

		$this->wipe_action_scheduler();

		$this->build_info->add_info( 'plugin_version', Env::$version );
		$this->logger->log( sprintf( 'Plugin version: %s', Env::$version ) );

		if ( $this->multilingual->is_multilingual() ) {
			$this->logger->log(
				sprintf( 'Multilingual: Yes, Provider: %s, Languages: %s, Default: %s', $this->multilingual->get_provider_name(), implode( ',', $this->multilingual->get_languages() ), $this->multilingual->get_default_language() )
			);
			$this->build_info->add_info( 'languages', $this->multilingual->get_languages() );
		}

		$max_total_sales = intval( $wpdb->get_var( "SELECT MAX(total_sales) FROM $wpdb->wc_product_meta_lookup" ) );
		$this->build_info->add_info( 'max_total_sales', $max_total_sales );

		$this->logger->log( 'Indexer prepared for building the index' );

		/**
		 * The indexer has changed status to: 'prepared'
		 *
		 * @since 0.0.1 Adding a hook.
		 */
		do_action( 'fibofilters/indexer/status/prepared' );

		return true;
	}

	public function build_index_process(): void {
		$this->build_info->set_is_indexing( true );

		$status = $this->build_info->get_info( 'status', $this->config->get_index_role() );

		if ( $status === 'building' ) {
			$this->logger->log( 'Indexer already running' );

			return;
		} elseif ( $status !== 'preparing' ) {
			$this->logger->log( 'Indexer is not prepared for running' );

			return;
		}

		$this->build_info->add_info( 'status', 'building' );

		$this->logger->log( 'Indexer started building the index' );

		/**
		 * The indexer has changed status to: 'started'
		 *
		 * @since 0.0.1
		 */
		do_action( 'fibofilters/indexer/status/started' );

		if ( empty( $this->store_filters->get_all_filters() ) ) {
			$this->logger->log( 'Skipping index build due to no filters defined' );
			$this->indexer_filters->complete();

			return;
		}

		$to_the_queue = [
			'filters' => [],
		];

		// -----------------------------------
		// FILTERS INDEX
		// -----------------------------------
		$this->database_filters->create( $this->config->get_index_role() );

		$source        = $this->source_query_factory->get( 'filters', [ 'select_type' => 'ids' ] );
		$product_ids   = $source->get_data();
		$total_objects = count( $product_ids );

		$source_counters       = $this->source_query_factory->get( 'filters', [ 'select_type' => 'counters' ] );
		$source_query_counters = $source_counters->get_data();
		if ( ! empty( $source_query_counters ) ) {
			$simple_products = count(
				array_filter(
					$source_query_counters,
					function ( $item ) {
						return $item['product_type'] === 'simple';
					}
				)
			);
			$this->build_info->add_info( 'simple_products_count', $simple_products );
			$variable_products = count(
				array_filter(
					$source_query_counters,
					function ( $item ) {
						return $item['product_type'] === 'variable';
					}
				)
			);
			$this->build_info->add_info( 'variable_products_count', $variable_products );
			$this->build_info->add_info( 'variations_count', $total_objects - $variable_products - $simple_products );
			$this->build_info->add_info( 'total_products_for_indexing', $total_objects - $variable_products );
		}

		$this->build_info->add_info( 'start_filters_ts', time() );
		$this->logger->log( '[Filters index] Building...' );

		$to_the_queue['filters'] = array_merge( $to_the_queue['filters'], $product_ids );

		if ( $this->config->is_indexer_mode( 'direct' ) ) {
			$this->process_queue_direct( $to_the_queue );
		} else {
			$this->process_queue_async( $to_the_queue );
		}

		$this->wp_helper->unlock_lock( self::INDEXING_PREPARE_PROCESS_EXIST_KEY );
	}

	private function process_queue_direct( array $to_the_queue ): void {
		// Process and complete.
		$this->indexer_filters->set_index_role( $this->config->get_index_role() );
		$result = $this->indexer_filters->index_set(
			$to_the_queue['filters'],
			function () {}
		);
		if ( $result ) {
			$this->indexer_filters->complete();
		}
	}

	private function process_queue_async( array $to_the_queue ): void {
		// Push to queue.
		foreach ( $to_the_queue as $indexer => $data ) {
			$this->push_to_queue( $data, $indexer );
		}

		$this->async_process->save();

		// Dispatch.
		if ( $this->async_process->get_save_result() ) {
			$this->async_process->maybe_dispatch();
		} else {
			$this->logger->log( '[Indexer] [Error code: 006] Failed to save indexer queue to the database.', 'emergency', 'both' );
			$this->cancel_build_index( 'error', 'Stop building the index. Starting the cancellation process.' );
		}
	}

	/**
	 * Push data to the correct queue
	 *
	 * @param array  $ids Product IDs.
	 * @param string $indexer Indexer type.
	 */
	public function push_to_queue( array $ids, string $indexer ): void {
		$limit  = 50;
		$object = null;
		$set    = [];

		switch ( $indexer ) {
			case 'filters':
				$object = $this->async_process;
				$limit  = apply_filters( 'fibofilters/indexer/filters_set_items_count', self::FILTERS_SET_ITEMS_COUNT );
				break;
		}

		// Stop early.
		if ( empty( $ids ) || ! is_object( $object ) ) {
			return;
		}

		$i = 0;
		foreach ( $ids as $id ) {
			$set[] = $id;

			if ( count( $set ) === $limit || $i + 1 === count( $ids ) ) {
				$object->push_to_queue( $set );
				$set = [];
			}

			$i++;
		}
	}

	/**
	 * Stops build index and wipes all processes and data
	 *
	 * @param string $status New indexer status.
	 * @param string $message Log message.
	 * @param bool   $clear_info Clear info (start time of processes).
	 */
	public function cancel_build_index( string $status = '', string $message = '', bool $clear_info = true ): void {
		if ( ! empty( $status ) ) {
			$this->build_info->add_info( 'status', $status );
		}
		if ( ! empty( $message ) ) {
			$this->logger->log( $message );
		}

		$this->async_process->cancel_process();

		if ( $this->wipe_index_tables( $this->config->get_index_role() ) ) {
			$this->logger->log( '[Filters index] Cleared' );
		}

		$this->db_util->remove_indexer_options();

		if ( $clear_info ) {
			$this->build_info->add_info( 'start_filters_ts', '' );
		}

		$this->wp_helper->unlock_lock( self::INDEXING_PREPARE_PROCESS_EXIST_KEY );

		/**
		 * The indexer has changed status to: $status ['cancellation', 'error'].
		 *
		 * @since 0.0.1
		 */
		if ( ! empty( $status ) ) {
			do_action( 'fibofilters/indexer/status/' . $status );
		}
	}

	/**
	 * Wipe index tables
	 *
	 * @param string $index_role Index role.
	 */
	public function wipe_index_tables( string $index_role = 'main' ): bool {
		return $this->database_filters->filters_index_exists( $index_role, false ) && $this->indexer_filters->wipe( $index_role );
	}

	/**
	 * Check if index is completed and valid
	 *
	 * @param string $index_role Index role.
	 * @param string $language Language.
	 */
	public function is_index_valid( string $index_role = 'main', string $language = '' ): bool {
		$languages = [ $language ];
		if ( empty( $language ) && $this->multilingual->is_multilingual() ) {
			$languages = $this->multilingual->get_languages();
		}

		foreach ( $languages as $current_language ) {
			if (
				$this->build_info->get_info( 'status', $index_role ) !== 'completed' ||
				! $this->database_filters->filters_index_exists( $index_role, true, $current_language )
			) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Removal of planned actions that will update products in the index
	 */
	public function wipe_action_scheduler(): void {
		$queue = $this->woocommerce_helper->get_queue();
		if ( empty( $queue ) ) {
			return;
		}

		$queue->cancel_all( 'fibofilters/indexer/background_product_update' );
	}

	public function maybe_completed(): void {
		if ( $this->mark_as_completed() ) {
			$this->copy_tmp_index_to_main();
			$this->logger->log( 'Indexing completed' );
			$this->descriptors_cache->process_descriptors_files();
			do_action( 'fibofilters/indexer/status/completed' );
		}
	}

	/**
	 * Complete the search index
	 */
	private function mark_as_completed(): bool {
		$status   = $this->build_info->get_info( 'status', $this->config->get_index_role() );
		$f_end_ts = absint( $this->build_info->get_info( 'end_filters_ts', $this->config->get_index_role() ) );

		if ( $status === 'building' && ! empty( $f_end_ts ) ) {
			$this->build_info->add_info( 'status', 'completed' );
			$this->build_info->add_info( 'end_ts', time() );

			return true;
		}

		return false;
	}

	private function copy_tmp_index_to_main(): void {
		global $wpdb;

		// Break early if parallel index building is disabled.
		if ( ! $this->config->is_parallel_building_enabled() ) {
			$this->logger->log( '[Indexer] Skipping copying of "tmp" tables - parallel building is disabled', 'debug', 'file' );

			return;
		}
		$languages = $this->multilingual->is_multilingual() ? $this->multilingual->get_languages() : [ '' ];
		foreach ( $languages as $language ) {
			if ( ! $this->is_index_valid( 'tmp', $language ) ) {
				$this->logger->log( '[Indexer] Skipping copying of "tmp" tables - "tmp" index is invalid', 'debug', 'file' );

				return;
			}
		}

		$tables = $this->db_util->get_all_plugin_tables();
		foreach ( $tables as $table ) {
			// Remove all "main" index tables (even those that don't have "tmp" version).
			if ( strpos( $table, '_main' ) !== false ) {
				$wpdb->query( "DROP TABLE IF EXISTS $table" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			}
			if ( strpos( $table, '_tmp' ) !== false ) {
				$main_table = str_replace( '_tmp', '_main', $table );
				// Make sure the table at index "main" does not exist.
				$wpdb->query( "DROP TABLE IF EXISTS $main_table" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
				$wpdb->query( "RENAME TABLE $table TO $main_table" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			}
		}

		foreach ( $this->build_info->get_index_info_struct() as $key => $value ) {
			if ( $key === 'logs' ) {
				$this->build_info->copy_indexer_last_build_logs_tmp_to_main();
				continue;
			}
			$value = get_option( self::LAST_BUILD_OPTION_KEY . '_' . $key . '_tmp', $value );
			delete_option( self::LAST_BUILD_OPTION_KEY . '_' . $key );

			/*
			 * Make sure the options are copied.
			 * Sometimes it happened that some options were not copied, and we have to be sure that everything went OK.
			 */
			$count = 0;
			do {
				$count++;
				if ( $count > 1 ) {
					usleep( 500000 );
				}
				if ( $count === 1 ) {
					$add_result  = add_option( self::LAST_BUILD_OPTION_KEY . '_' . $key, $value, '', 'no' );
					$saved_value = get_option( self::LAST_BUILD_OPTION_KEY . '_' . $key );
					$continue    = ! $add_result || $value !== $saved_value;
				} else {
					delete_option( self::LAST_BUILD_OPTION_KEY . '_' . $key );
					$wpdb->query( $wpdb->prepare( "INSERT INTO $wpdb->options (`option_name`, `option_value`, `autoload`) VALUES (%s, %s, 'no')", self::LAST_BUILD_OPTION_KEY . '_' . $key, maybe_serialize( $value ) ) );
					$continue = false;
				}
				if ( $key === 'logs' ) {
					break;
				}
			} while ( $continue && $count < 3 );
		}
	}

	public function check_if_indexer_is_working_too_long(): void {
		$last_action_ts = absint( $this->build_info->get_info( 'last_action_ts', $this->config->get_index_role() ) );
		$diff           = ! empty( $last_action_ts ) ? time() - $last_action_ts : 0;

		if (
			$diff >= MINUTE_IN_SECONDS && $diff <= ( MINUTE_IN_SECONDS + 1 )
			|| $diff >= ( 3 * MINUTE_IN_SECONDS ) && $diff <= ( 3 * MINUTE_IN_SECONDS + 1 )
			|| $diff >= ( 5 * MINUTE_IN_SECONDS ) && $diff <= ( 5 * MINUTE_IN_SECONDS + 1 )
		) {
			$this->logger->log( sprintf( '[Indexer] %d minute(s) with no action', floor( $diff / 60 ) ), 'debug', 'file' );

			// Last chance to finish indexing.
			$this->maybe_completed();
		}

		if ( $this->build_info->is_indexer_working_too_long() ) {
			if ( $this->wp_helper->has_wp_cron_missed_events() ) {
				$this->logger->log( sprintf( '[Indexer] [Error code: 001] The index build was stuck for %d minutes.', floor( $diff / 60 ) ), 'emergency', 'both' );
			} else {
				$this->logger->log( sprintf( '[Indexer] [Error code: 002] The index build was stuck for %d minutes.', floor( $diff / 60 ) ), 'emergency', 'both' );
			}

			$this->cancel_build_index( 'error', 'Stop building the index. Starting the cancellation process.' );
		}
	}
}
