<?php

namespace AutomateWoo\Carts;

use AutomateWoo\Cart;
use AutomateWoo\Logger;
use WC_Cart;
use WC_Session;

/**
 * Class CartRestorer
 *
 * Restores a saved cart to the current session.
 *
 * @since 5.4.0
 */
class CartRestorer {

	/**
	 * @var Cart
	 */
	protected $stored_cart;

	/**
	 * @var WC_Cart
	 */
	protected $current_cart;

	/**
	 * @var WC_Session
	 */
	protected $current_session;

	/**
	 * The key of the cart item that is currently being restored.
	 *
	 * @var string
	 */
	protected $current_cart_item_key;

	/**
	 * Unique key/identifier of the cart item that is currently being restored.
	 *
	 * @var string
	 */
	protected $current_unfiltered_cart_item_key;

	/**
	 * CartRestorer constructor.
	 *
	 * @param Cart       $stored_cart The stored cart to be restored.
	 * @param WC_Cart    $current_cart
	 * @param WC_Session $current_session
	 */
	public function __construct( Cart $stored_cart, WC_Cart $current_cart, WC_Session $current_session ) {
		$this->stored_cart     = $stored_cart;
		$this->current_cart    = $current_cart;
		$this->current_session = $current_session;
	}

	/**
	 * Restore the stored cart items and coupons to the current session cart.
	 *
	 * @return bool True if the cart was restored, false on failure.
	 */
	public function restore(): bool {
		if ( ! $this->stored_cart->has_items() ) {
			// Nothing to restore
			return false;
		}

		$notices_backup = wc_get_notices();

		$this->restore_items();
		$this->restore_coupons();

		// Revert success notices added when re-adding coupons and products, but keep restore errors visible.
		$this->current_session->set( 'wc_notices', $this->get_restored_notices( $notices_backup, wc_get_notices() ) );

		do_action( 'automatewoo/cart/restored', $this->stored_cart );

		return true;
	}

	/**
	 * Get notices to restore after rebuilding the cart.
	 *
	 * @param array $notices_backup Notices present before restore.
	 * @param array $current_notices Notices present after restore.
	 *
	 * @return array
	 */
	protected function get_restored_notices( array $notices_backup, array $current_notices ): array {
		foreach ( $current_notices as $notice_type => $notices ) {
			if ( 'success' === $notice_type ) {
				continue;
			}

			$backup_notice_count = isset( $notices_backup[ $notice_type ] ) ? count( $notices_backup[ $notice_type ] ) : 0;
			$new_notices         = array_slice( $notices, $backup_notice_count );

			if ( $new_notices ) {
				$notices_backup[ $notice_type ] = array_merge( $notices_backup[ $notice_type ] ?? [], $new_notices );
			}
		}

		return $notices_backup;
	}

	/**
	 * Restore cart items.
	 */
	protected function restore_items() {
		$existing_items = $this->current_cart->get_cart_for_session();

		foreach ( $this->stored_cart->get_items() as $item ) {
			if ( isset( $existing_items[ $item->get_key() ] ) ) {
				continue; // item already exists in cart
			}

			// Use a filter to force the new cart item key to match the key of the stored cart item
			// This is important to prevent duplicate cart items upon login or if multiple cart restores occur
			$this->current_cart_item_key = $item->get_key();

			// Calculate a unique identifier for this cart item that we can check to ensure that the callback
			// hooked to 'woocommerce_cart_id' filter will modify the cart id of this item,
			// and not other items potentially added to the cart via hooks fired in 'WC_Cart::add_to_cart'.
			$this->current_unfiltered_cart_item_key = WC()->cart->generate_cart_id( $item->get_product_id(), $item->get_variation_id(), $item->get_variation_data(), $item->get_data() );

			add_filter( 'woocommerce_cart_id', [ $this, 'filter_cart_key_to_force_new_key_to_match_stored_key' ], 10, 5 );

			// \WC_Cart::add_to_cart wrongly contains a @throws tag
			try {
				$this->current_cart->add_to_cart(
					$item->get_product_id(),
					$item->get_quantity(),
					$item->get_variation_id(),
					$item->get_variation_data(),
					$item->get_data()
				);
			} catch ( \Exception $e ) {
				Logger::error( 'cart-restore', $e->getMessage() );
			}

			remove_filter( 'woocommerce_cart_id', [ $this, 'filter_cart_key_to_force_new_key_to_match_stored_key' ], 10, 5 );
		}
	}

	/**
	 * Restore stored cart coupons to current cart.
	 */
	protected function restore_coupons() {
		foreach ( $this->stored_cart->get_coupons() as $coupon_code => $coupon_data ) {
			if ( ! $this->current_cart->has_discount( $coupon_code ) ) {
				$this->current_cart->add_discount( $coupon_code );
			}
		}
	}

	/**
	 * Filter the cart item key to preserve the key of the item that was just restored.
	 *
	 * @param  string $cart_id
	 * @param  int    $product_id      contains the id of the product to add to the cart.
	 * @param  int    $variation_id    ID of the variation being added to the cart.
	 * @param  array  $variation       attribute values.
	 * @param  array  $cart_item_data  extra cart item data we want to pass into the item.
	 *
	 * @return string
	 */
	public function filter_cart_key_to_force_new_key_to_match_stored_key( string $cart_id, $product_id, $variation_id, $variation, $cart_item_data ): string {

		if ( $this->current_cart_item_key ) {

			// Calculate a unique identifier for this cart item
			// and compare it against the one stored in the 'current_unfiltered_cart_item_key' property.
			// If it's the same, we know that we are filtering the cart item id we intended to.
			remove_filter( 'woocommerce_cart_id', [ $this, 'filter_cart_key_to_force_new_key_to_match_stored_key' ] );
			$unfiltered_cart_id = WC()->cart->generate_cart_id( $product_id, $variation_id, $variation, $cart_item_data );
			add_filter( 'woocommerce_cart_id', [ $this, 'filter_cart_key_to_force_new_key_to_match_stored_key' ], 10, 5 );

			if ( $this->current_unfiltered_cart_item_key === $unfiltered_cart_id ) {
				return $this->current_cart_item_key;
			}
		}

		return $cart_id;
	}
}
