<?php
declare( strict_types=1 );

namespace FiboFilters\Indexer;

use FiboFilters\Filters\FilterSources;
use FiboFilters\Filters\StoreFilters;
use FiboFilters\Integrations\Multilingual;
use FiboFilters\Product\ProductFactory;
use FiboFilters\WooCommerce\Helper as WooCommerceHelper;

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

/**
 * Updater Class
 */
class Updater {

	/**
	 * Temporary buffer to store already processed IDs to prevent double actions
	 *
	 * @var array
	 */
	private $already_processed = [];

	/**
	 * Allowed taxonomies
	 *
	 * @var null|string[]
	 */
	private $allowed_taxonomies;

	/** @var BackgroundProductUpdater */
	private $background_product_updater;

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

	/** @var ProductFactory */
	private $product_factory;

	/** @var FilterSources */
	private $filter_sources;

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

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

	public function __construct(
		BackgroundProductUpdater $background_product_updater,
		Multilingual $multilingual,
		ProductFactory $product_factory,
		FilterSources $filter_sources,
		StoreFilters $store_filters,
		WooCommerceHelper $woocommerce_helper
	) {
		$this->background_product_updater = $background_product_updater;
		$this->multilingual               = $multilingual;
		$this->product_factory            = $product_factory;
		$this->filter_sources             = $filter_sources;
		$this->store_filters              = $store_filters;
		$this->woocommerce_helper         = $woocommerce_helper;
	}

	public function init(): void {
		// Break early if updater isn't enabled.
		if ( ! apply_filters( 'fibofilters/indexer/updater/enabled', true ) ) {
			return;
		}

		// Products.
		add_action( 'woocommerce_new_product', [ $this, 'on_product_update' ], 20, 1 );
		add_action( 'woocommerce_update_product', [ $this, 'on_product_update' ], 20, 1 );
		add_action( 'woocommerce_update_product_variation', [ $this, 'on_product_update' ], 20, 1 );

		add_action( 'woocommerce_delete_product', [ $this, 'on_product_delete' ], 20 );
		add_action( 'woocommerce_before_delete_product_variation', [ $this, 'on_product_delete' ], 20 );
		add_action( 'woocommerce_trash_product', [ $this, 'on_product_delete' ], 20 );
		add_action( 'woocommerce_trash_product_variation', [ $this, 'on_product_delete' ], 20 );

		add_action( 'save_post', [ $this, 'on_product_update' ], 10000, 1 );
		add_action( 'deleted_post', [ $this, 'on_product_delete' ], 10000 );

		// Terms.
		add_action( 'edited_term', [ $this, 'on_term_save' ], 10000, 3 );
		add_action( 'created_term', [ $this, 'on_term_save' ], 10000, 3 );

		add_action( 'delete_term', [ $this, 'on_term_delete' ], 10000, 5 );
	}

	/**
	 * Set allowed taxonomies
	 */
	public function set_allowed_taxonomies(): void {
		if ( is_null( $this->allowed_taxonomies ) ) {
			$this->allowed_taxonomies = [];
			$sources                  = $this->store_filters->get_all_filter_sources();
			foreach ( $sources as $source_id ) {
				$source = $this->filter_sources->get_source( $source_id );
				if ( $source->get_data_attr( 'sourceType' ) === 'taxonomy' ) {
					$this->allowed_taxonomies[] = $source->get_data_attr( 'rawValue' );
				}
			}
		}
	}

	/**
	 * Update the index if the product was changed or created
	 *
	 * @param int $product_id Product ID.
	 */
	public function on_product_update( int $product_id ): void {
		// Break early if updater isn't enabled.
		if ( ! apply_filters( 'fibofilters/indexer/updater/enabled', true ) ) {
			return;
		}

		$this->set_allowed_taxonomies();

		$product_obj = $this->product_factory->get( $product_id );

		if ( ! $product_obj->is_valid() ) {
			return;
		}

		if ( $this->is_already_processed( 'update', $product_obj->get_id() ) ) {
			return;
		}

		// Variation ID? Get parent product.
		if ( $product_obj->is_type( 'variation' ) ) {
			$product_obj = $this->product_factory->get( $product_obj->get_wc_product()->get_parent_id() );
			if ( ! $product_obj->is_valid() || $this->is_already_processed( 'update', $product_obj->get_id() ) ) {
				return;
			}
		}

		$can_index = $product_obj->is_published_and_visible() && apply_filters( 'fibofilters/indexer/updater/can_index', $product_obj->can_index(), $product_obj->get_id(), $product_obj->get_wc_product() );
		if ( $can_index ) {
			if ( in_array( $product_obj->get_wc_product()->get_type(), $this->woocommerce_helper->get_variations_wrapper_product_types() ) ) {
				$variations = $product_obj->get_wc_product()->get_available_variations(); // @phpstan-ignore-line
				foreach ( $variations as $variation ) {
					$this->schedule_action( 'update', $variation['variation_id'] );
				}
			} else {
				$this->schedule_action( 'update', $product_obj->get_id() );
			}
		} else {
			$this->schedule_action( 'delete', $product_obj->get_id() );
		}
	}

	/**
	 * Remove a product from the index
	 *
	 * @param int|\WC_Product|\WP_Post $product_id Product ID or object.
	 */
	public function on_product_delete( $product_id ): void {
		// Break early if updater isn't enabled.
		if ( ! apply_filters( 'fibofilters/indexer/updater/enabled', true ) ) {
			return;
		}

		$product = $this->product_factory->get( $product_id );

		if ( ! $product->is_valid() ) {
			return;
		}

		if ( ! $this->is_already_processed( 'delete', $product->get_id() ) ) {
			$this->schedule_action( 'delete', $product->get_id() );
		}
	}

	/**
	 * Remove a term from the index
	 *
	 * @param int    $term_id Term ID.
	 * @param int    $tt_id Term taxonomy ID.
	 * @param string $taxonomy Taxonomy slug.
	 * @param mixed  $deleted_term Copy of the already-deleted term, in the form specified
	 *                              by the parent function. WP_Error otherwise.
	 * @param array  $object_ids List of term object IDs.
	 */
	public function on_term_delete( $term_id, $tt_id, $taxonomy, $deleted_term, $object_ids ): void {
		// Break early if updater isn't enabled.
		if ( ! apply_filters( 'fibofilters/indexer/updater/enabled', true ) ) {
			return;
		}

		$this->set_allowed_taxonomies();

		if ( is_array( $this->allowed_taxonomies ) && in_array( $taxonomy, $this->allowed_taxonomies, true ) ) {
			// Maybe reindex related products.
			$object_ids = array_map( 'intval', $object_ids );
			$this->maybe_update_products( $object_ids, $taxonomy );
		}
	}

	/**
	 * Update the index if a term has been changed.
	 *
	 * @param int    $term_id Term ID.
	 * @param int    $tt_id Term taxonomy ID.
	 * @param string $taxonomy Taxonomy slug.
	 */
	public function on_term_save( $term_id, $tt_id, $taxonomy ): void {
		// Break early if updater isn't enabled.
		if ( ! apply_filters( 'fibofilters/indexer/updater/enabled', true ) ) {
			return;
		}

		$this->set_allowed_taxonomies();

		if ( ! is_array( $this->allowed_taxonomies ) ) {
			return;
		}

		if ( ! in_array( $taxonomy, $this->allowed_taxonomies, true ) ) {
			return;
		}

		$current_lang = $this->multilingual->is_multilingual() ? $this->multilingual->get_current_language() : '';

		// Check if a given term is associated with any object.
		$terms = get_terms(
			[
				'taxonomy'   => $taxonomy,
				'hide_empty' => true,
				'include'    => [ $term_id ],
			]
		);

		if ( ! is_wp_error( $terms ) && ! empty( $terms ) ) {
			// Maybe reindex related products.
			$post_ids = get_objects_in_term( $term_id, $taxonomy );
			if ( ! is_wp_error( $post_ids ) && ! empty( $post_ids ) ) {
				$post_ids = array_map( 'intval', $post_ids );
				$this->maybe_update_products( $post_ids, $taxonomy );
			}
		}

		if ( ! empty( $current_lang ) ) {
			$this->multilingual->switch_language( $current_lang );
		}
	}

	/**
	 * Update products in index if user can search in related taxonomy
	 *
	 * @param int[]  $post_ids Product IDs.
	 * @param string $taxonomy Taxonomy slug.
	 */
	private function maybe_update_products( array $post_ids, string $taxonomy ): void {
		if (
			( is_array( $this->allowed_taxonomies ) && in_array( $taxonomy, $this->allowed_taxonomies, true ) ) ||
			in_array( $taxonomy, $this->woocommerce_helper->get_attribute_taxonomy_names(), true )
		) {
			foreach ( $post_ids as $post_id ) {
				$post_type = get_post_type( $post_id );
				if ( $post_type === 'product' ) {
					$this->on_product_update( $post_id );
				}
			}
		}
	}

	/**
	 * Schedule action
	 *
	 * @param string $action Action to schedule.
	 * @param int    $post_id Product ID.
	 */
	public function schedule_action( string $action, int $post_id ): void {
		// Whitelist of actions.
		if ( ! in_array( $action, [ 'update', 'delete' ], true ) ) {
			return;
		}

		if ( $this->is_already_processed( $action, $post_id ) ) {
			return;
		}

		$this->background_product_updater->schedule( $action, $post_id );

		// We can always mark as processed.
		$this->already_processed[ $action ][] = $post_id;
	}

	/**
	 * Check if product or post was already processed to prevent process it twice
	 *
	 * @param string $action Action to schedule.
	 * @param int    $post_id Product ID.
	 */
	private function is_already_processed( string $action, int $post_id ): bool {
		$processed = false;

		if (
			isset( $this->already_processed[ $action ] ) &&
			in_array( $post_id, $this->already_processed[ $action ], true )
		) {
			$processed = true;
		}

		return $processed;
	}
}
