<?php
// phpcs:disable WordPressVIPMinimum.Variables.RestrictedVariables.cache_constraints___COOKIE, WordPressVIPMinimum.Functions.RestrictedFunctions.cookies_setcookie

namespace TEC\Tickets\Commerce;

use TEC\Tickets\Commerce;
use TEC\Tickets\Commerce\Cart\Cart_Interface;
use TEC\Tickets\Commerce\Traits\Cart as Cart_Trait;
use Tribe__Tickets__Tickets as Tickets;
use Tribe__Tickets__Tickets_Handler as Tickets_Handler;
use Tribe__Tickets_Plus__Meta__Storage as Meta_Storage;
use Tribe__Utils__Array as Arr;

/**
 * Class Cart
 *
 * @since   5.1.9
 *
 * @package TEC\Tickets\Commerce
 */
class Cart {

	use Cart_Trait;

	/**
	 * Which URL param we use to identify a given page as the cart.
	 * Keep in mind this is not the only way, please use `is_current_page()` to determine that.
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	public static $url_query_arg = 'tec-tc-cart';

	/**
	 * Which URL param we use to tell the checkout page to set a cookie, since you cannot set a cookie on a 302
	 * redirect.
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	public static $cookie_query_arg = 'tec-tc-cookie';

	/**
	 * Redirect mode string, which will be used to determine which kind of cart the repository might be.
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	const REDIRECT_MODE = 'redirect';

	/**
	 * Which URL param we use to identify a given page as the cart.
	 * Keep in mind this is not the only way, please use `is_current_page()` to determine that.
	 *
	 * @since 5.1.9
	 *
	 * @var string[]
	 */
	protected $available_modes = [ self::REDIRECT_MODE ];

	/**
	 * Which cookie we will store the cart hash.
	 *
	 * @since 5.1.9
	 *
	 * @var string
	 */
	public static $cart_hash_cookie_name = 'tec-tickets-commerce-cart';

	/**
	 * Gets the current instance of cart handling that we are using.
	 * Most of the pieces should be handled in the Repository for the cart, only piece fully handled by the
	 * parent class is the cookie handling.
	 *
	 * @since 5.1.9
	 * @since 5.21.0 Updated to use Cart_Interface instead of Unmanaged_Cart.
	 *
	 * @return Cart_Interface
	 */
	public function get_repository() {
		$default_cart = tribe( Cart_Interface::class );

		/**
		 * Filters the cart repository, by default we use Unmanaged Cart.
		 *
		 * @since 5.1.9
		 * @since 5.21.0 Updated to use Cart_Interface instead of Unmanaged_Cart.
		 *
		 * @param Cart_Interface $cart Instance of the cart repository managing the cart.
		 */
		return apply_filters( 'tec_tickets_commerce_cart_repository', $default_cart );
	}

	/**
	 * From the current active cart repository we fetch it's mode.
	 *
	 * @since 5.1.9
	 *
	 * @return string
	 */
	public function get_mode() {
		return $this->get_repository()->get_mode();
	}

	/**
	 * Gets the list of available modes we can use for the cart.
	 *
	 * @since 5.1.9
	 *
	 * @return string[]
	 */
	public function get_available_modes() {
		return $this->available_modes;
	}

	/**
	 * If a given string is a valid and available mode.
	 *
	 * @since 5.1.9
	 *
	 * @param string $mode Which mode we are testing.
	 *
	 * @return bool
	 */
	public function is_available_mode( $mode ) {
		return in_array( $mode, $this->get_available_modes(), true );
	}

	/**
	 * If the current page is the cart page or not.
	 *
	 * @since 5.1.9
	 * @since 5.21.1 Ensure it method returns false if tickets commerce is disabled.
	 *
	 * @return bool
	 */
	public function is_current_page() {
		if ( ! tec_tickets_commerce_is_enabled() ) {
			return false;
		}

		$cart_mode = tribe_get_request_var( static::$url_query_arg, false );

		if ( ! $this->is_available_mode( $cart_mode ) ) {
			return false;
		}

		// When the current cart doesn't use this mode we fail the page check.
		if ( $this->get_mode() !== $cart_mode ) {
			return false;
		}

		return true;
	}

	/**
	 * Determine the Current cart Transient Key based on invoice number.
	 *
	 * @since 5.1.9
	 *
	 * @return string|null
	 */
	public function get_current_cart_transient() {
		$cart_hash = $this->get_cart_hash();

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

		return $this->get_transient_key( $cart_hash );
	}

	/**
	 * Determine the Current cart URL.
	 *
	 * @since 5.1.9
	 *
	 * @return string
	 */
	public function get_url() {
		$url = home_url( '/' );

		$url = add_query_arg( [ static::$url_query_arg => $this->get_mode() ], $url );

		/**
		 * Allows modifications to the cart url for Tickets Commerce.
		 *
		 * @since 5.1.9
		 *
		 * @param string $url URL for the cart.
		 */
		return (string) apply_filters( 'tec_tickets_commerce_cart_url', $url );
	}

	/**
	 * Generates a unique version of the cart hash, used to enforce idempotency in REST API requests.
	 *
	 * @since 5.4.0.2
	 *
	 * @param string $salt An optional value to make sure the generated hash is not directly translatable to the cart
	 *                     hash.
	 *
	 * @return string
	 */
	public function generate_cart_order_hash( $salt = '' ): string {
		$cart_hash = $this->get_cart_hash();

		/**
		 * Allows modifications to the cart/order hash for Tickets Commerce.
		 *
		 * @since 5.4.0.2
		 *
		 * @param string $cart_order_hash The md5-hashed cart hash.
		 * @param string $cart_hash       The current cart hash.
		 * @param string $salt            The salt value.
		 */
		return (string) apply_filters( 'tec_tickets_commerce_cart_order_hash', md5( $cart_hash . $salt ), $cart_hash, $salt );
	}

	/**
	 * Reads the cart hash from the cookies.
	 *
	 * @since 5.1.9
	 *
	 * @return string|null The cart hash or `null` if not found.
	 */
	public function get_cart_hash( $generate = false ) {
		$cart_hash_length = 12;
		$cart_hash        = $this->get_repository()->get_hash();
		$hash_from_cookie = sanitize_text_field( $_COOKIE[ static::get_cart_hash_cookie_name() ] ?? '' );

		if ( strlen( $hash_from_cookie ) === $cart_hash_length ) {
			$cart_hash           = $hash_from_cookie;
			$cart_hash_transient = get_transient( $this->get_transient_key( $cart_hash ) );

			if ( empty( $cart_hash_transient ) ) {
				$cart_hash = null;
			}
		}

		if ( empty( $cart_hash ) && $generate ) {
			$tries     = 1;
			$max_tries = 20;

			// While we dont find an empty transient to store this cart we loop, but avoid more than 20 tries.
			while (
				( ! empty( $cart_hash_transient ) || empty( $cart_hash ) )
				&& $max_tries >= $tries
			) {
				$cart_hash           = wp_generate_password( $cart_hash_length, false );
				$cart_hash_transient = get_transient( $this->get_transient_key( $cart_hash ) );

				// Make sure we increment.
				++$tries;
			}
		}

		$this->set_cart_hash( $cart_hash );

		return $this->get_repository()->get_hash();
	}

	/**
	 * Configures the Cart hash on the class object
	 *
	 * @since 5.2.0
	 *
	 * @param string $cart_hash Cart hash value.
	 *
	 */
	public function set_cart_hash( $cart_hash ) {
		$this->get_repository()->set_hash( $cart_hash );
	}

	/**
	 * Determine if the cart has items.
	 *
	 * @since 5.19.3
	 *
	 * @return bool Whether the cart has items.
	 */
	public function has_items(): bool {
		return $this->get_repository()->has_items();
	}

	/**
	 * Clear the cart.
	 *
	 * @since 5.1.9
	 *
	 * @return bool
	 */
	public function clear_cart() {
		$this->set_cart_hash_cookie( null );
		$this->get_repository()->clear();

		unset( $_COOKIE[ static::get_cart_hash_cookie_name() ] );

		return delete_transient( static::get_current_cart_transient() );
	}

	/**
	 * Based on the current time when is the cart going to expire.
	 *
	 * @since 5.19.3
	 *
	 * @return int
	 */
	public function get_cart_expiration(): int {
		/**
		 * Filters the life span of the Cart Cookie.
		 *
		 * @depecated
		 * @since 5.1.9
		 * @since 5.21.0 Deprecated in favor of `tec_tickets_commerce_cart_cookie_expiration`.
		 *
		 * @param int $expire The expiry time, as passed to setcookie().
		 */
		$expire = (int) apply_filters_deprecated(
			'tec_tickets_commerce_cart_expiration',
			[ time() + HOUR_IN_SECONDS ],
			'5.21.0',
			'tec_tickets_commerce_cart_cookie_expiration'
		);

		/**
		 * Filters the life span of the Cart Cookie.
		 *
		 * @since 5.21.0
		 *
		 * @param int $expire The expiry time, as passed to setcookie().
		 */
		return (int) apply_filters( 'tec_tickets_commerce_cart_cookie_expiration', $expire );
	}

	/**
	 * Sets the cart hash cookie or resets the cookie.
	 *
	 * @since 5.1.9
	 *
	 * @param string $value Value used for the cookie or empty to purge the cookie.
	 *
	 * @return boolean
	 */
	public function set_cart_hash_cookie( $value = '' ) {
		if ( headers_sent() ) {
			return false;
		}

		$expire = $this->get_cart_expiration();

		// When null means we are deleting.
		if ( null === $value ) {
			$expire = 1;
		}

		$is_cookie_set = setcookie( static::get_cart_hash_cookie_name(), $value ?? '', $expire, COOKIEPATH ?: '/', COOKIE_DOMAIN, is_ssl(), true );

		if ( $is_cookie_set ) {
			// Overwrite local variable, so we can use it right away.
			$_COOKIE[ static::get_cart_hash_cookie_name() ] = $value;
		}

		return $is_cookie_set;
	}

	/**
	 * Get the tickets currently in the cart for a given provider.
	 *
	 * @since 5.1.9
	 * @since 5.21.0 Added the $type parameter.
	 *
	 * @param bool   $full_item_params Determines all the item params, including event_id, sub_total, and obj.
	 * @param string $type             The type of item to get from the cart. Default is 'ticket'. Use 'all' to get all items.
	 *
	 * @return array List of items.
	 */
	public function get_items_in_cart( $full_item_params = false, string $type = 'ticket' ) {
		return $this->get_repository()->get_items_in_cart( $full_item_params, $type );
	}

	/**
	 * Handles the process of adding a ticket product to the cart.
	 *
	 * If the cart contains a line item for the product, this will replace the previous quantity.
	 * If the quantity is zero and the cart contains a line item for the product, this will remove it.
	 *
	 * @since 5.1.9
	 *
	 * @param int   $ticket_id  Ticket ID.
	 * @param int   $quantity   Ticket quantity to add.
	 * @param array $extra_data Extra data to send to the cart item.
	 */
	public function add_ticket( $ticket_id, $quantity = 1, array $extra_data = [] ) {
		$cart = $this->get_repository();

		// Ensure quantity is an integer.
		$quantity = (int) $quantity;

		// Add to / update quantity in cart.
		$cart->upsert_item( $ticket_id, $quantity, $extra_data );
	}

	/**
	 * Handles the process of adding a ticket product to the cart.
	 *
	 * If the cart contains a line item for the product, this will replace the previous quantity.
	 * If the quantity is zero and the cart contains a line item for the product, this will remove it.
	 *
	 * @since 5.1.9
	 *
	 * @param int  $ticket_id Ticket ID.
	 * @param ?int $quantity  Ticket quantity to remove. If null, the item will be removed.
	 */
	public function remove_ticket( $ticket_id, ?int $quantity = null ) {
		$cart = $this->get_repository();

		// If the quantity is null, we remove the item.
		if ( null === $quantity ) {
			$cart->remove_item( $ticket_id );
			return;
		}

		// Make sure the cart actually has the item.
		if ( ! $cart->has_item( $ticket_id ) ) {
			return;
		}

		$current_quantity = $cart->get_item_quantity( $ticket_id );
		$new_quantity     = max( 0, $current_quantity - $quantity );

		$cart->upsert_item( $ticket_id, $new_quantity );
	}

	/**
	 * If product cache parameter is found, delete saved products from temporary cart.
	 *
	 * @filter wp_loaded 0
	 *
	 * @since  5.1.9
	 */
	public function maybe_delete_expired_products() {
		$delete = tribe_get_request_var( 'clear_product_cache', null );

		if ( empty( $delete ) ) {
			return;
		}

		$transient_key = $this->get_current_cart_transient();

		// Bail if we have no data key.
		if ( empty( $transient_key ) ) {
			return;
		}

		// Bail if ET+ is not in place.
		if ( ! class_exists( Meta_Storage::class ) ) {
			return;
		}

		$storage = new Meta_Storage();

		// Bail if we have no data to delete.
		$transient = get_transient( $transient_key );
		if ( empty( $transient ) ) {
			return;
		}

		foreach ( $transient as $ticket_id => $data ) {
			$storage->delete_cookie( $ticket_id );
		}
	}

	/**
	 * Prepare the data for cart processing.
	 *
	 * Note that most of the data that is processed here is legacy, so you will see very weird and wonky naming.
	 * Make sure when you are making modifications you consider:
	 * - Event Tickets without ET+ additional data
	 * - Event Ticket Plus IAC
	 * - Event Tickets Plus Attendee Registration
	 *
	 * @since 5.1.9
	 *
	 * @param array $request_data Request Data to be prepared.
	 *
	 * @return array
	 */
	public function prepare_data( $request_data ) {
		/**
		 * Filters the Cart data before sending to the prepare method.
		 *
		 * @since 5.1.9
		 *
		 * @param array $request_data The cart data before processing.
		 */
		$request_data = apply_filters( 'tec_tickets_commerce_cart_pre_prepare_data', $request_data );

		if ( empty( $request_data['tribe_tickets_ar_data'] ) ) {
			return [];
		}

		$raw_data = $request_data['tribe_tickets_ar_data'];

		// Attempt to JSON decode data if needed.
		if ( ! is_array( $raw_data ) ) {
			$raw_data = stripslashes( $raw_data );
			$raw_data = json_decode( $raw_data, true );
		}

		// Set up the raw data from the request.
		$raw_data = array_merge( $request_data, $raw_data );

		// Set the initial data array.
		$data = [
			'post_id'  => absint( Arr::get( $raw_data, 'tribe_tickets_post_id' ) ),
			'provider' => sanitize_text_field( Arr::get( $raw_data, 'tribe_tickets_provider', Module::class ) ),
			'tickets'  => Arr::get( $raw_data, 'tribe_tickets_tickets' ),
			'meta'     => Arr::get( $raw_data, 'tribe_tickets_meta', [] ),
		];

		// Set up the ticket data and metadata.
		$tickets_meta    = Arr::get( $raw_data, 'tribe_tickets', [] );
		$data['tickets'] = array_filter( $this->map_ticket_data( $data['tickets'], $tickets_meta ) );

		/**
		 * Filters the Meta on the Data before processing.
		 *
		 * @since 5.1.9
		 *
		 * @param array $meta Meta information on the cart.
		 * @param array $data Data used for the cart.
		 */
		$data['meta'] = apply_filters( 'tec_tickets_commerce_cart_prepare_data_meta', $data['meta'], $data );

		/**
		 * Filters the Cart data before sending to to the Cart Repository.
		 *
		 * @since 5.1.9
		 *
		 * @param array $data The cart data after processing.
		 */
		return (array) apply_filters( 'tec_tickets_commerce_cart_prepare_data', $this->get_repository()->prepare_data( $data ) );
	}

	/**
	 * Prepares the data from the Tickets form.
	 *
	 * @since 5.1.9
	 *
	 * @return bool
	 */
	public function parse_request() {
		// When it's not the current page we just bail.
		if ( ! $this->is_current_page() ) {
			return false;
		}

		$data = $this->prepare_data( $_POST );

		/**
		 * Hook to inject behavior before cart is processed, if you need to change the data that will be used, you
		 * should look into `tec_tickets_commerce_cart_prepare_data`.
		 *
		 * @since 5.1.9
		 *
		 * @param array $data Data used to process the cart.
		 */
		do_action( 'tec_tickets_commerce_cart_before_process', $data );

		$processed = $this->process( $data );

		/**
		 * Hook to inject behavior after cart is processed.
		 *
		 * @since 5.1.9
		 *
		 * @param array $data      Data used to process the cart.
		 * @param bool  $processed Whether or not we processed the data.
		 */
		do_action( 'tec_tickets_commerce_cart_after_process', $data, $processed );

		if ( static::REDIRECT_MODE === $this->get_mode() ) {
			$redirect_url = tribe( Checkout::class )->get_url();

			/**
			 * Filter the base redirect URL for cart to checkout.
			 *
			 * @since 5.2.0
			 *
			 * @param string $redirect_url Redirect URL.
			 * @param array  $data         Data that we just processed on the cart.
			 */
			$redirect_url = apply_filters( 'tec_tickets_commerce_cart_to_checkout_redirect_url_base', $redirect_url, $data );

			if (
				! isset( $_COOKIE[ $this->get_cart_hash() ] )
				|| ! $_COOKIE[ $this->get_cart_hash() ]
			) {
				$redirect_url = add_query_arg( [ static::$cookie_query_arg => $this->get_cart_hash() ], $redirect_url );
			}

			/**
			 * Which url it redirects after the processing of the cart.
			 *
			 * @since 5.1.9
			 *
			 * @param string $redirect_url Which url we will direct after processing the cart. Defaults to Checkout page.
			 * @param array  $data         Data that we just processed on the cart.
			 */
			$redirect_url = apply_filters( 'tec_tickets_commerce_cart_to_checkout_redirect_url', $redirect_url, $data );

			if ( null !== $redirect_url ) {
				wp_safe_redirect( $redirect_url );
				tribe_exit();
			}
		}

		return true;
	}

	/**
	 * Process a given cart data into this cart instance.
	 *
	 * @since 5.1.9
	 *
	 * @param array $data
	 *
	 * @return array|boolean Boolean true when it was a success or an array of errors.
	 */
	public function process( array $data = [] ) {
		if ( empty( $data ) ) {
			return false;
		}

		// Before we start we clear the existing cart.
		return $this->get_repository()->process( $data );
	}

	/**
	 * Get the total of the cart.
	 *
	 * @since 5.10.0
	 *
	 * @return null|float
	 */
	public function get_cart_total() {
		return $this->get_repository()->get_cart_total();
	}

	/**
	 * Get the subtotal of the cart.
	 *
	 * @since 5.18.0
	 *
	 * @return float
	 */
	public function get_cart_subtotal(): float {
		return $this->get_repository()->get_cart_subtotal();
	}

	/**
	 * Get cart has cookie name.
	 *
	 * @since 5.18.1
	 *
	 * @return string
	 */
	public static function get_cart_hash_cookie_name(): string {
		/**
		 * Filters the cart hash cookie name.
		 *
		 * @since 5.18.1
		 *
		 * @param string $cart_hash_cookie_name The cart hash cookie name.
		 *
		 * @return string
		 */
		$filtered_cookie_name = apply_filters( 'tec_tickets_commerce_cart_hash_cookie_name', static::$cart_hash_cookie_name );

		return sanitize_title( $filtered_cookie_name );
	}

	/**
	 * Map the ticket data to a more usable format.
	 *
	 * @since 5.21.0
	 *
	 * @param array $ticket_data  Array of raw ticket data.
	 * @param array $tickets_meta Array of ticket meta data.
	 *
	 * @return array Array of mapped ticket data.
	 */
	protected function map_ticket_data( array $ticket_data, array $tickets_meta ): array {
		/** @var Tickets_Handler $handler */
		$handler = tribe( 'tickets.handler' );

		return array_map(
			static function ( $ticket ) use ( $handler, $tickets_meta ) {
				// Merge the ticket data with the default ticket data.
				$ticket = array_merge(
					[
						'ticket_id' => 0,
						'quantity'  => 0,
						'optout'    => false,
						'iac'       => 'none',
						'extra'     => [],
					],
					$ticket
				);

				// Normalize and validate the quantity.
				$ticket['quantity'] = (int) $ticket['quantity'];
				if ( $ticket['quantity'] < 1 ) {
					return false;
				}

				// Normalize and validate the ticket ID.
				$ticket['ticket_id'] = (int) $ticket['ticket_id'];
				if ( true !== $handler->is_ticket_readable( $ticket['ticket_id'] ) ) {
					return false;
				}

				// Add attendee data.
				if ( ! empty( $tickets_meta[ $ticket['ticket_id'] ]['attendees'] ) ) {
					$ticket['extra']['attendees'] = $tickets_meta[ $ticket['ticket_id'] ]['attendees'];
				}

				// Add the optout and IAC data.
				$ticket['extra']['optout'] = tribe_is_truthy( $ticket['optout'] );
				$ticket['extra']['iac']    = sanitize_text_field( $ticket['iac'] );
				unset( $ticket['optout'], $ticket['iac'] );

				// Add the ticket object.
				$ticket['obj'] = Tickets::load_ticket_object( $ticket['ticket_id'] );

				return $ticket;
			},
			$ticket_data
		);
	}

	/**
	 * Returns the name of the transient used by the cart.
	 *
	 * @since 5.1.9
	 * @depecated Use `get_transient_key()` via the Cart trait instead.
	 *
	 * @param string $id The cart id.
	 *
	 * @return string The transient name.
	 */
	public static function get_transient_name( $id ) {
		_deprecated_function( __METHOD__, '5.21.0', 'TEC\Tickets\Commerce\Traits\Cart::get_transient_key' );
		return Commerce::ABBR . '-cart-' . md5( $id ?? '' );
	}
}
