<?php
/**
 * This file belongs to the YIT Framework.
 *
 * This source file is subject to the GNU GENERAL PUBLIC LICENSE (GPL 3.0)
 * that is bundled with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://www.gnu.org/licenses/gpl-3.0.txt
 *
 * @package YITH\PreOrder\Includes
 * @author YITH <plugins@yithemes.com>
 */

if ( ! defined( 'YITH_WCPO_VERSION' ) ) {
	exit( 'Direct access forbidden.' );
}

if ( ! class_exists( 'YITH_Pre_Order_Orders_Manager' ) ) {
	/**
	 * Class YITH_Pre_Order_Orders_Manager
	 */
	class YITH_Pre_Order_Orders_Manager {

		/**
		 * Pre-ordered status
		 *
		 * @var string
		 */
		public static $pre_ordered_status = 'wc-ywpo-pre-ordered';

		/**
		 * Main Instance
		 *
		 * @var YITH_Pre_Order_Orders_Manager
		 * @since  2.0.0
		 */
		protected static $instance;

		/**
		 * Returns single instance of the class
		 *
		 * @return YITH_Pre_Order_Orders_Manager
		 * @since 2.0.0
		 */
		public static function get_instance() {
			if ( is_null( self::$instance ) ) {
				self::$instance = new self();
			}

			return self::$instance;
		}

		/**
		 * Construct
		 */
		public function __construct() {
			add_action( 'init', array( $this, 'register_pre_ordered_status' ) );
			add_filter( 'wc_order_statuses', array( $this, 'add_pre_ordered_to_wc_order_statuses' ) );
			add_filter( 'woocommerce_email_actions', array( $this, 'add_transactional_email' ) );
			add_action( 'init', array( $this, 'manage_new_pre_order_email_hooks' ) );
			add_action( 'woocommerce_checkout_update_order_meta', array( $this, 'add_charge_type_meta' ) );
			add_filter( 'woocommerce_cancel_unpaid_order', array( $this, 'prevent_cancel_pending_payment_pre_orders' ), 10, 2 );
			add_filter( 'woocommerce_valid_order_statuses_for_payment_complete', array( $this, 'pre_ordered_status_valid_for_payment_complete' ) );
			add_action( 'woocommerce_payment_complete', array( $this, 'update_pending_payment_meta' ) );
			add_filter( 'woocommerce_payment_complete_order_status', array( $this, 'update_payment_complete_order_status' ), 10, 2 );
			if ( 'pre_ordered' === get_option( 'ywpo_order_status', 'default' ) ) {
				$this->set_pre_order_status_for_offline_gateways();
			}

			// AJAX actions.
			add_action( 'wp_ajax_ywpo_complete_pre_order', array( $this, 'ajax_complete_pre_order' ) );
			add_action( 'wp_ajax_ywpo_cancel_pre_order', array( $this, 'ajax_cancel_pre_order' ) );
			add_action( 'wp_ajax_ywpo_mark_pre_order_as_paid', array( $this, 'ajax_mark_pre_order_as_paid' ) );
			add_action( 'wp_ajax_ywpo_pre_order_item_action', array( $this, 'ajax_pre_order_item_action' ) );
		}

		/**
		 * Register the pre-ordered status.
		 */
		public function register_pre_ordered_status() {
			register_post_status(
				self::get_pre_ordered_status(),
				array(
					'label'                     => apply_filters(
						'ywpo_pre_ordered_status_label',
						__( 'Pre-ordered', 'yith-pre-order-for-woocommerce' )
					),
					'public'                    => true,
					'exclude_from_search'       => false,
					'show_in_admin_all_list'    => true,
					'show_in_admin_status_list' => true,
					'label_count'               => apply_filters(
						'ywpo_pre_ordered_status_label_count',
						/* translators: %s: Count */
						_n_noop(
							'Pre-ordered <span class="count"> (%s)</span>',
							'Pre-ordered <span class="count"> (%s)</span>',
							'yith-pre-order-for-woocommerce'
						)
					),
				)
			);
		}

		/**
		 * Include the Pre-Ordered order status in the WooCommerce order statuses array.
		 *
		 * @param  array $status WooCommerce order statuses array.
		 *
		 * @return array
		 */
		public function add_pre_ordered_to_wc_order_statuses( $status ) {
			$status[ self::get_pre_ordered_status() ] = apply_filters( 'ywpo_pre_ordered_status_label', __( 'Pre-ordered', 'yith-pre-order-for-woocommerce' ) );

			return $status;
		}

		/**
		 * Include the Pre-Ordered status to the WooCommerce transactional emails actions.
		 *
		 * @param array $actions WooCommerce transactional emails actions.
		 *
		 * @return array
		 */
		public function add_transactional_email( $actions ) {
			$status = self::get_pre_ordered_status();
			$status = 'wc-' === substr( $status, 0, 3 ) ? substr( $status, 3 ) : $status;

			$actions[] = 'woocommerce_order_status_pending_to_' . $status;
			$actions[] = 'woocommerce_order_status_failed_to_' . $status;
			$actions[] = 'woocommerce_order_status_cancelled_to_' . $status;
			$actions[] = 'woocommerce_order_status_on-hold_to_' . $status;
			return $actions;
		}

		/**
		 * Include the transactional email hooks for the 'Pre-order confirmed' and 'New pre-order' email notifications.
		 */
		public function manage_new_pre_order_email_hooks() {
			$status = self::get_pre_ordered_status();
			$status = 'wc-' === substr( $status, 0, 3 ) ? substr( $status, 3 ) : $status;

			add_action( 'woocommerce_order_status_pending_to_on-hold_notification', array( $this, 'new_pre_order_email_trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_failed_to_on-hold_notification', array( $this, 'new_pre_order_email_trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_cancelled_to_on-hold_notification', array( $this, 'new_pre_order_email_trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_pending_to_processing_notification', array( $this, 'new_pre_order_email_trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_failed_to_processing_notification', array( $this, 'new_pre_order_email_trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_cancelled_to_processing_notification', array( $this, 'new_pre_order_email_trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_on-hold_to_processing_notification', array( $this, 'new_pre_order_email_trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_pending_to_' . $status . '_notification', array( $this, 'new_pre_order_email_trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_failed_to_' . $status . '_notification', array( $this, 'new_pre_order_email_trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_cancelled_to_' . $status . '_notification', array( $this, 'new_pre_order_email_trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_on-hold_to_' . $status . '_notification', array( $this, 'new_pre_order_email_trigger' ), 10, 2 );
			add_action( 'woocommerce_order_status_completed_notification', array( $this, 'new_pre_order_email_trigger' ), 10, 2 );
		}

		/**
		 * Send the 'Pre-order confirmed' and 'New pre-order' email notifications.
		 *
		 * @param int|string $order_id The WC Order ID.
		 * @param WC_Order   $order    The WC Order object.
		 */
		public function new_pre_order_email_trigger( $order_id, $order ) {
			global $sitepress;
			if ( ! $order instanceof WC_Order || 'yes' !== $order->get_meta( '_order_has_preorder' ) ) {
				return;
			}
			$items = $order->get_items();
			// Send the email notification.
			WC()->mailer();
			foreach ( $items as $item ) {
				if ( 'line_item' !== $item->get_type() || 'yes' === $item->get_meta( '_ywpo_new_pre_order_email_sent' ) ) {
					return;
				}
				$id      = $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id();
				$product = $sitepress ? wc_get_product( yit_wpml_object_id( $id, 'product', true, $sitepress->get_default_language() ) ) : wc_get_product( $id );

				$item_preorder = $item->get_meta( '_ywpo_item_preorder' );
				$item_status   = $item->get_meta( '_ywpo_item_status' );

				if ( 'yes' === $item_preorder && 'waiting' === $item_status ) {
					do_action( 'ywpo_confirmed_email', $order, $product, $item->get_id() );
					do_action( 'ywpo_new_pre_order_email', $order, $product, $item->get_id() );
				}
			}
		}

		/**
		 * Add the charge type meta to the order when it's processed via Checkout.
		 *
		 * @param int $order_id The order ID.
		 */
		public function add_charge_type_meta( $order_id ) {
			$order = wc_get_order( $order_id );
			if ( ywpo_order_has_pre_order( $order ) ) {
				$charge_type = ywpo_get_cart_charge_type();
				if ( $charge_type ) {
					// If the cart charge type is 'pay_later', set the order charge type as 'upon_release'.
					$charge_type = 'pay_later' === $charge_type ? 'upon_release' : $charge_type;
					ywpo_set_order_charge_type( $charge_type, $order );
				}

				// For upfront mode, set the pre-order as pending payment first. If the order is paid then,
				// update_pending_payment_meta() will update the meta.
				if ( 'upfront' === $charge_type ) {
					$order->update_meta_data( '_ywpo_pending_payment', 'yes' );
					$order->save();
				}
			}
		}

		/**
		 * Prevent pending payment completed pre-orders from being cancelled.
		 *
		 * @param bool     $cancel_order Whether to cancel the pending order or not.
		 * @param WC_Order $order The WC_Order object.
		 *
		 * @return bool
		 */
		public function prevent_cancel_pending_payment_pre_orders( $cancel_order, $order ) {
			if (
				ywpo_order_has_pre_order( $order ) &&
				ywpo_is_completed_pre_order( $order ) &&
				'yes' === $order->get_meta( '_ywpo_pending_payment' )
			) {
				$cancel_order = false;
			}

			return $cancel_order;
		}

		/**
		 * Add the Pre-Ordered status to the valid order statuses for payment complete array.
		 *
		 * @param array $statuses Valid order statuses.
		 * @return array
		 */
		public function pre_ordered_status_valid_for_payment_complete( $statuses ) {
			$status     = self::get_pre_ordered_status();
			$statuses[] = 'wc-' === substr( $status, 0, 3 ) ? substr( $status, 3 ) : $status;
			return $statuses;
		}

		/**
		 * Set that the upon release pre-order is not pending for the payment anymore.
		 *
		 * @param string $order_id The order ID.
		 */
		public function update_pending_payment_meta( $order_id ) {
			$order = wc_get_order( $order_id );
			if ( 'yes' === $order->get_meta( '_ywpo_pending_payment' ) ) {
				$order->update_meta_data( '_ywpo_pending_payment', 'no' );
				$order->save();
			}
		}

		/**
		 * Set the order status for upfront pre-orders according to the configuration of
		 * the "Order status for pre-orders" option.
		 * For the order status for upon release pre-orders, see self::set_as_pre_order_pending_payment()
		 *
		 * @param string       $status   The new order status to be changed.
		 * @param int|WC_Order $order_id If the function is called from the filter
		 * 'woocommerce_payment_complete_order_status' it's the order ID. If it's called
		 * from YITH_Pre_Order_Orders_Manager::set_pre_order_status_for_offline_gateways() it's the WC_Order object.
		 *
		 * @return string
		 */
		public function update_payment_complete_order_status( $status, $order_id ) {
			if ( 'default' === get_option( 'ywpo_order_status', 'default' ) ) {
				return $status;
			}

			$order = wc_get_order( $order_id );
			if (
				! $order instanceof WC_Order
				|| ! ywpo_order_has_pre_order( $order )
				|| ywpo_is_upon_release_order( $order )
				|| ywpo_is_completed_pre_order( $order )
			) {
				return $status;
			}

			return apply_filters( 'ywpo_upfront_payment_complete_status', self::get_pre_ordered_status(), $order, $status );
		}

		// AJAX actions.

		/**
		 * AJAX action to force the completion of all pre-order items of an order. This action is fired from the 'Pre-Orders List' table.
		 */
		public function ajax_complete_pre_order() {
			if ( current_user_can( 'edit_shop_orders' ) && check_admin_referer( 'ywpo-complete-pre-order' ) && isset( $_GET['order_id'] ) ) {
				$order_id = absint( wp_unslash( $_GET['order_id'] ) );
				$orders   = array( $order_id );
				self::complete_pre_orders( $orders, apply_filters( 'ywpo_ajax_complete_pre_order_force', true, $order_id ) );
			}
			wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'admin.php?page=yith_wcpo_panel&tab=pre-orders&sub_tab=orders' ) );
			exit;
		}

		/**
		 * AJAX action to cancel all pre-order items of an order.
		 * This action is fired from the 'Pre-Orders List' table.
		 */
		public function ajax_cancel_pre_order() {
			if ( current_user_can( 'edit_shop_orders' ) && check_admin_referer( 'ywpo-cancel-pre-order' ) && isset( $_GET['order_id'] ) ) {
				$order_id = absint( wp_unslash( $_GET['order_id'] ) );
				self::cancel_pre_order( $order_id );
			}
			wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'admin.php?page=yith_wcpo_panel&tab=pre-orders&sub_tab=orders' ) );
			exit;
		}

		/**
		 * AJAX action to manually mark an order as paid. This action is fired from the 'Pre-Orders List' table.
		 */
		public function ajax_mark_pre_order_as_paid() {
			if ( current_user_can( 'edit_shop_orders' ) && check_admin_referer( 'ywpo-mark-pre-order-as-paid' ) && isset( $_GET['order_id'] ) ) {
				$order_id = absint( wp_unslash( $_GET['order_id'] ) );
				ywpo_mark_as_paid( $order_id );
			}
			wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'admin.php?page=yith_wcpo_panel&tab=pre-orders&sub_tab=orders' ) );
			exit;
		}

		/**
		 * AJAX action to cancel or force the completion of a pre-order item (not whole order).
		 * This action is fired from the 'Manage Products' modal.
		 */
		public function ajax_pre_order_item_action() {
			check_ajax_referer( 'manage-products-modal-item-action', 'security' );

			$action_type = ! empty( $_POST['action_type'] ) ? sanitize_text_field( wp_unslash( $_POST['action_type'] ) ) : false;
			$item_id     = ! empty( $_POST['item_id'] ) ? sanitize_text_field( wp_unslash( $_POST['item_id'] ) ) : false;
			$order_id    = ! empty( $_POST['order_id'] ) ? sanitize_text_field( wp_unslash( $_POST['order_id'] ) ) : false;

			$order = wc_get_order( $order_id );
			if ( $action_type & $item_id && $order instanceof WC_Order ) {
				$order = wc_get_order( $order_id );
				switch ( $action_type ) {
					case 'complete':
						self::complete_upfront_pre_order( $order, $item_id, true );
						break;
					case 'cancel':
						self::cancel_pre_order( $order, $item_id );
						break;
				}
				wp_send_json_success();
			} else {
				wp_send_json_error( 'Missing "item_id" or "order_id"' );
			}
		}

		// Static functions.

		/**
		 * Retrieves the Pre-Ordered status.
		 *
		 * @return string
		 */
		public static function get_pre_ordered_status() {
			return apply_filters( 'ywpo_get_pre_ordered_status', self::$pre_ordered_status );
		}

		/**
		 * Retrieves the offline gateways actions when the order has been placed in order to set the pre-order status.
		 *
		 * @return array
		 */
		public static function get_offline_gateways_hooks() {
			return apply_filters(
				'ywpo_get_offline_gateways_hooks',
				array(
					'woocommerce_bacs_process_payment_order_status',
					'woocommerce_cheque_process_payment_order_status',
					'woocommerce_cod_process_payment_order_status',
				)
			);
		}

		/**
		 * Delete the date paid metas through the delete_post_meta() function because these props are protected.
		 *
		 * @param WC_Order $order The WC_Order object.
		 */
		public static function delete_date_paid_meta( $order ) {
			delete_post_meta( $order->get_id(), '_date_paid' );
			delete_post_meta( $order->get_id(), '_paid_date' );
		}

		/**
		 * Changes the status for an unpaid, but payment-tokenized order to pre-ordered and adds meta to indicate the order
		 * has a payment token. Should be used by supported gateways when processing a pre-order charged upon release, instead of calling
		 * $order->payment_complete(), this will be used. Note that if the order used pay later, this does not apply.
		 *
		 * @param WC_Order|int $order The WC_Order object or the order ID.
		 */
		public static function set_as_pre_order_pending_payment( $order ) {
			$order = wc_get_order( $order );

			if ( ! ( $order instanceof WC_Order ) || ywpo_is_pay_later_order( $order ) ) {
				return;
			}

			do_action( 'ywpo_before_pre_order_pending_payment_metas', $order );

			// Set that the order has a payment token, which will be used upon release to charge pre-order total amount.
			$order->update_meta_data( '_ywpo_has_payment_token', 1 );
			$order->update_meta_data( '_ywpo_pending_payment', 'yes' );

			$order->update_status(
				'default' === get_option( 'ywpo_order_status', 'default' ) ?
					apply_filters( 'woocommerce_payment_complete_order_status', $order->needs_processing() ? 'processing' : 'completed', $order->get_id(), $order ) :
					self::get_pre_ordered_status()
			);

			// If the status is updated with 'processing' or 'completed' status, the date paid metas will be added despite there is no payment yet.
			// This function deletes the metas for maintaining coherence.
			self::delete_date_paid_meta( $order );

			do_action( 'ywpo_after_pre_order_pending_payment_metas', $order );

			wc_maybe_reduce_stock_levels( $order->get_id() );
		}

		/**
		 * Complete a Pre-Order that is pending payment.
		 *
		 * @param WC_Order|int $order The WC_Order instance or order ID.
		 * @param bool         $force Use the value `true` to force the completion of the pre-order even if there is
		 * no release date, or it has not passed yet.
		 */
		public static function complete_upon_release_pre_order( $order, $force = false ) {
			$order = wc_get_order( $order );

			if (
				! $order instanceof WC_Order ||
				in_array(
					$order->get_status(),
					apply_filters( 'ywpo_invalid_order_statuses_pre_order_complete', array( 'cancelled', 'refunded', 'failed' ) ),
					true
				)
			) {
				return;
			}

			do_action( 'ywpo_before_complete_upon_release_pre_order', $order );

			$pre_order_items = $order->get_meta( '_ywpo_pre_order_items' );
			$item_id         = '';
			if ( is_array( $pre_order_items ) ) {
				// Since Upon Release Pre-Orders only contains one Order Item Product, the first key is taken without looping over the array.
				$item_id = key( $pre_order_items );
			}

			$item = new WC_Order_Item_Product( $item_id );
			if ( ! $item instanceof WC_Order_Item_Product ) {
				unset( $pre_order_items[ $item_id ] );
				$order->update_meta_data( '_ywpo_pre_order_items', $pre_order_items );
				$order->save();
				return;
			}

			$item_preorder     = $item->get_meta( '_ywpo_item_preorder' );
			$item_status       = $item->get_meta( '_ywpo_item_status' );
			$item_release_date = $item->get_meta( '_ywpo_item_for_sale_date' );
			$item_variation_id = $item->get_variation_id();
			$item_product_id   = is_numeric( $item_variation_id ) && (int) $item_variation_id > 0
				? $item_variation_id
				: $item->get_product_id();

			if ( 'yes' === $item_preorder && 'waiting' === $item_status ) {
				if ( ( ! empty( $item_release_date ) && is_numeric( $item_release_date ) ) && (int) $item_release_date <= time() || $force ) {
					if ( $order->get_total() > 0 ) {
						// update order status to 'pending' so it can be paid by automatic payment,
						// or on pay page by customer if 'pay later' gateway was used.
						$order->update_status( 'pending' );

						if ( ywpo_order_has_payment_token( $order ) ) {
							// load payment gateways.
							WC()->payment_gateways();

							// Trigger payment hook.
							do_action( 'ywpo_process_pre_order_release_payment_' . $order->get_payment_method(), $order );
							do_action( 'ywpo_process_pre_order_release_payment_generic', $order, $item_id );
						}

						do_action( 'ywpo_complete_upon_release_pre_order_after_processing_payment', $order, $item_id );

						// Even if the payment fails, the pre-order is marked as completed to avoid payment retry.
						$item->update_meta_data( '_ywpo_item_status', 'completed' );
						$item->save_meta_data();

						$pre_order_items[ $item_id ] = 'completed';
						$order->update_meta_data( '_ywpo_pre_order_items', $pre_order_items );
						$order->save();

						$product = wc_get_product( $item_product_id );
						if ( $product instanceof WC_Product && 'yes' === get_option( 'yith_wcpo_enable_pre_order_purchasable', 'yes' ) ) {
							ywpo_reset_pre_order( $product );
						}

						if ( ywpo_all_items_are_completed( $order ) ) {
							ywpo_set_pre_order_completed( $order );
						}

						// Send email notification.
						WC()->mailer();
						do_action( 'ywpo_completed_email', $order, $product, $item_id );
					} else {
						// If the order total is 0, no payment is needed, so it's treated as an upfront pre-order.
						self::complete_upfront_pre_order( $order );
					}
				}
			}

			do_action( 'ywpo_after_complete_upon_release_pre_order', $order );
		}

		/**
		 * Complete a pre-order that does not need payment. Since an upfront pre-order can contain several pre-orders
		 * with different release dates, the items are processed individually.
		 *
		 * @param WC_Order|int    $order   The WC_Order instance or order ID.
		 * @param string|int|bool $item_id The Order Item ID in case to complete a specific pre-order item only.
		 * @param bool            $force   Use the value `true` to force the completion of the pre-order even if there is
		 * no release date, or it has not passed yet.
		 */
		public static function complete_upfront_pre_order( $order, $item_id = false, $force = false ) {
			$order = wc_get_order( $order );

			if (
				! $order instanceof WC_Order ||
				in_array(
					$order->get_status(),
					apply_filters( 'ywpo_invalid_order_statuses_pre_order_complete', array( 'cancelled', 'refunded', 'failed' ) ),
					true
				)
			) {
				return;
			}

			$pre_order_items       = $order->get_meta( '_ywpo_pre_order_items' );
			$processed_order_items = $pre_order_items;

			if ( ! $pre_order_items ) {
				return;
			}

			do_action( 'ywpo_before_complete_upfront_pre_order', $order );

			if ( $item_id ) {
				$item = new WC_Order_Item_Product( $item_id );
				if ( 'waiting' === $item->get_meta( '_ywpo_item_status' ) ) {
					$processed_order_items = self::complete_upfront_pre_order_item( $item_id, $order, $processed_order_items, apply_filters( 'ywpo_complete_upfront_pre_order_item_force', $force, $item_id, $order ) );

				}
			} else {
				foreach ( $pre_order_items as $item_id => $status ) {
					if ( 'waiting' === $status ) {
						$processed_order_items = self::complete_upfront_pre_order_item( $item_id, $order, $processed_order_items, $force );
					}
				}
			}

			if ( $pre_order_items !== $processed_order_items ) {
				$order->update_meta_data( '_ywpo_pre_order_items', $processed_order_items );
				$order->save();
				wc_maybe_reduce_stock_levels( $order->get_id() );

				// Mark the order as cancelled or completed as appropriate.
				self::finish_pre_order_and_update_order_status( $order );
			}

			do_action( 'ywpo_after_complete_upfront_pre_order', $order );
		}

		/**
		 * Complete a specific pre-order item.
		 *
		 * @param string|int $item_id         The Order Item ID.
		 * @param WC_Order   $order           The WC_Order object.
		 * @param array      $pre_order_items Pre-Order items array.
		 * @param bool       $force           Use the value `true` to force the completion of the pre-order even if
		 *                                    there is no release date, or it has not passed yet.
		 * @return array
		 */
		private static function complete_upfront_pre_order_item( $item_id, $order, $pre_order_items, $force = false ) {
			try {
				$item = new WC_Order_Item_Product( $item_id );
			} catch ( Exception $exception ) {
				$item = false;
			}
			if ( ! $item ) {
				unset( $pre_order_items[ $item_id ] );
				return $pre_order_items;
			}

			$item_preorder     = $item->get_meta( '_ywpo_item_preorder' );
			$item_status       = $item->get_meta( '_ywpo_item_status' );
			$item_release_date = $item->get_meta( '_ywpo_item_for_sale_date' );
			$item_variation_id = $item->get_variation_id();
			$item_product_id   = is_numeric( $item_variation_id ) && (int) $item_variation_id > 0
				? $item_variation_id
				: $item->get_product_id();

			if ( 'yes' === $item_preorder && 'waiting' === $item_status ) {
				if ( ( ! empty( $item_release_date ) && is_numeric( $item_release_date ) ) && (int) $item_release_date <= time() || $force ) {
					// Mark the item as completed in the order item meta.
					$item->update_meta_data( '_ywpo_item_status', 'completed' );
					$item->save_meta_data();

					// Mark the item as completed for the '_ywpo_pre_order_items' meta.
					$pre_order_items[ $item_id ] = 'completed';

					$product = wc_get_product( $item_product_id );
					if ( $product instanceof WC_Product && 'yes' === get_option( 'yith_wcpo_enable_pre_order_purchasable', 'yes' ) ) {
						ywpo_reset_pre_order( $product );
					}
				}
			}
			return $pre_order_items;
		}

		/**
		 * Completes pre-orders by a given IDs array
		 *
		 * @param array $orders Order IDs array.
		 * @param bool  $force  Use the value `true` to force the completion of the pre-order even if there is
		 *                      no release date, or it has not passed yet.
		 */
		public static function complete_pre_orders( $orders, $force = false ) {
			do_action( 'ywpo_before_complete_pre_orders', $orders );

			$orders = apply_filters( 'ywpo_complete_pre_orders', $orders );
			foreach ( $orders as $order_id ) {
				if ( ywpo_is_completed_pre_order( $order_id ) ) {
					continue;
				}
				try {
					if ( ywpo_is_upon_release_order( $order_id ) || ywpo_is_pay_later_order( $order_id ) ) {
						self::complete_upon_release_pre_order( $order_id, $force );
					} else {
						self::complete_upfront_pre_order( $order_id, false, $force );
					}
				} catch ( Exception $exception ) {
					error_log( print_r( $exception->getMessage(), true ) );
					return new WP_Error( $exception->getCode(), $exception->getMessage() );
				}
			}

			do_action( 'ywpo_after_complete_pre_orders', $orders );
		}

		/**
		 * Cancel a single pre-order item.
		 *
		 * @param string|int $item_id         The WC_Order_Item_Product object.
		 * @param WC_Order   $order           The WC_Order object.
		 * @param array      $pre_order_items Pre-Order items array.
		 *
		 * @return array
		 */
		private static function cancel_pre_order_item( $item_id, $order, $pre_order_items ) {
			$item    = new WC_Order_Item_Product( $item_id );
			$product = $item->get_product();

			do_action( 'ywpo_before_cancel_pre_order_item', $item, $order, $product, $pre_order_items );

			// Set the item status as cancelled.
			$item->update_meta_data( '_ywpo_item_status', 'cancelled' );
			$item->save_meta_data();
			$pre_order_items[ $item->get_id() ] = 'cancelled';

			// Translators: %s: product name.
			$order->add_order_note( apply_filters( 'ywpo_pre_order_cancelled_order_note', sprintf( __( 'Pre-order %s has been cancelled', 'yith-pre-order-for-woocommerce' ), $product->get_formatted_name() ), $order, $product, $item ) );

			do_action( 'ywpo_cancelled_email', $order, $product, $item->get_id() );

			wc_maybe_increase_stock_levels( $order->get_id() );

			do_action( 'ywpo_after_cancel_pre_order_item', $item, $order, $product, $pre_order_items );

			return $pre_order_items;
		}


		/**
		 * Cancel all pre-order items from an order.
		 *
		 * @param WC_Order|int    $order   The WC_Order instance or order ID.
		 * @param string|int|bool $item_id The Order Item ID. Used when only a specific pre-order item will be cancelled.
		 */
		public static function cancel_pre_order( $order, $item_id = false ) {
			$order           = wc_get_order( $order );
			$pre_order_items = $order->get_meta( '_ywpo_pre_order_items' );

			if ( ! $order instanceof WC_Order || ! $pre_order_items ) {
				return;
			}

			do_action( 'ywpo_before_cancel_pre_order', $order );

			// Instantiate WC_Emails.
			WC()->mailer();

			if ( $item_id ) {
				$item = new WC_Order_Item_Product( $item_id );
				if ( 'waiting' === $item->get_meta( '_ywpo_item_status' ) ) {
					// Process the item to cancel it.
					$pre_order_items = self::cancel_pre_order_item( $item_id, $order, $pre_order_items );
				}
			} else {
				foreach ( $pre_order_items as $item_id => $status ) {
					if ( 'waiting' === $status ) {
						// Process the item to cancel it.
						$pre_order_items = self::cancel_pre_order_item( $item_id, $order, $pre_order_items );
					}
				}
			}

			// Update directly the "_ywpo_pre_order_items" array.
			$order->update_meta_data( '_ywpo_pre_order_items', $pre_order_items );
			$order->save();
			wc_maybe_reduce_stock_levels( $order->get_id() );

			// Mark the order as cancelled or completed as appropriate.
			self::finish_pre_order_and_update_order_status( $order );

			do_action( 'ywpo_after_cancel_pre_order', $order );
		}

		/**
		 * Cancels pre-orders by a given array of IDs.
		 *
		 * @param array $orders Order IDs array.
		 */
		public static function cancel_pre_orders( $orders ) {
			do_action( 'ywpo_before_cancel_pre_orders', $orders );

			$orders = apply_filters( 'ywpo_cancel_pre_orders', $orders );
			foreach ( $orders as $order ) {
				if ( ywpo_is_completed_pre_order( $order ) ) {
					continue;
				}
				self::cancel_pre_order( $order );
			}

			do_action( 'ywpo_after_complete_pre_orders', $orders );
		}

		// Private functions.

		/**
		 * Add the actions from the offline gateways hooks. Additional hooks can be added
		 * in YITH_Pre_Order_Orders_Manager::get_offline_gateways_hooks().
		 */
		private function set_pre_order_status_for_offline_gateways() {
			$actions = self::get_offline_gateways_hooks();
			foreach ( $actions as $action ) {
				add_action( $action, array( $this, 'update_payment_complete_order_status' ), 10, 2 );
			}
		}

		/**
		 * Called when a pre-order is finished (completed or cancelled). Set the pre-order status and then update the
		 * order status as appropriate.
		 *
		 * @param WC_Order $order The WC_Order object.
		 */
		private static function finish_pre_order_and_update_order_status( $order ) {
			if ( ywpo_all_items_are_cancelled( $order ) ) {
				ywpo_set_pre_order_cancelled( $order );
				$order->update_status( 'cancelled' );
			} elseif ( ywpo_all_items_are_completed( $order ) ) {
				ywpo_set_pre_order_completed( $order );

				$pre_order_items = $order->get_meta( '_ywpo_pre_order_items' );
				// Instantiate WC_Emails.
				WC()->mailer();
				foreach ( $pre_order_items as $item_id => $status ) {
					if ( 'completed' === $status ) {
						$item = $order->get_item( $item_id );
						// Send email notification.
						do_action( 'ywpo_completed_email', $order, $item->get_product(), $item_id );
					}
				}

				$valid_statuses = apply_filters( 'woocommerce_valid_order_statuses_for_payment_complete', array( 'on-hold', 'pending', 'failed', 'cancelled' ), $order );
				// If all Pre-Order items are completed, set a payment complete status (if necessary). The order needs to be paid and the current order status should be a valid one.
				if ( ywpo_order_is_paid( $order ) && $order->has_status( $valid_statuses ) ) {
					$payment_complete_status = apply_filters( 'woocommerce_payment_complete_order_status', $order->needs_processing() ? 'processing' : 'completed', $order->get_id(), $order );
					$order->update_status( $payment_complete_status );
				}
			}
		}
	}
}

/**
 * Unique access to instance of YITH_Pre_Order_Orders_Manager class
 *
 * @return YITH_Pre_Order_Orders_Manager
 */
function YITH_Pre_Order_Orders_Manager() { // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.FunctionNameInvalid
	return YITH_Pre_Order_Orders_Manager::get_instance();
}
