<?php
// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

/**
 * WC_Appointment_Cart_Manager class.
 */
class WC_Appointment_Cart_Manager {
	use WC_Appointments_Manager_Trait;

	/**
	 * The class id used for identification in logging.
	 *
	 * @var string
	 */
	public string $id = 'wc_appointment_cart_manager';

	/**
	 * Constructor
	 */
	public function __construct() {
		if ( $this->is_active() ) {
			$this->register_hooks();
		}
	}

	/**
	 * Register WordPress hooks for this manager.
	 *
	 * @return void
	 */
	public function register_hooks(): void {
		add_action( 'woocommerce_before_add_to_cart_button', [ $this, 'add_product_object' ], 0 );
		add_action( 'woocommerce_before_appointment_form_output', [ $this, 'add_product_object' ], 0 );
		add_action( 'woocommerce_after_appointment_form_output', [ $this, 'add_product_object' ], 0 );
		add_action( 'woocommerce_appointment_add_to_cart', [ $this, 'add_to_cart' ], 30 );
		add_filter( 'woocommerce_cart_item_quantity', [ $this, 'cart_item_quantity' ], 15, 3 );
		add_filter( 'woocommerce_add_cart_item', [ $this, 'add_cart_item' ], 9, 1 ); #9 to allow others to hook after.
		add_filter( 'woocommerce_get_cart_item_from_session', [ $this, 'get_cart_item_from_session' ], 9, 3 ); #9 to allow others to hook after.
		add_action( 'woocommerce_cart_loaded_from_session', [ $this, 'cart_loaded_from_session' ], 10 );
		add_filter( 'woocommerce_get_item_data', [ $this, 'get_item_data' ], 10, 2 );
		add_filter( 'woocommerce_add_cart_item_data', [ $this, 'add_cart_item_data' ], 10, 2 );
		add_action( 'woocommerce_new_order_item', [ $this, 'order_item_meta' ], 50, 2 );
		add_action( 'woocommerce_store_api_checkout_order_processed', [ $this, 'review_items_on_block_checkout' ], 10, 1 );
		add_action( 'woocommerce_checkout_order_processed', [ $this, 'review_items_on_shortcode_checkout' ], 10, 1 );
		add_filter( 'woocommerce_add_to_cart_validation', [ $this, 'validate_appointment_posted_data' ], 10, 3 );
		add_filter( 'woocommerce_add_to_cart_validation', [ $this, 'validate_appointment_requires_confirmation' ], 20, 2 );
		add_filter( 'woocommerce_add_to_cart_validation', [ $this, 'validate_appointment_sold_individually' ], 20, 3 );
		add_filter( 'woocommerce_store_api_product_quantity_editable', [ $this, 'disable_cart_block_qty_field' ], 10, 3 );
		#add_action( 'woocommerce_after_checkout_validation', [ $this, 'validate_appointment_order_legacy_checkout' ], 999, 2 );
		#add_action( 'woocommerce_store_api_cart_errors', [ $this, 'validate_appointment_order_checkout_block_support' ], 999, 2 );
		add_action( 'woocommerce_cart_item_removed', [ $this, 'cart_item_removed' ], 20 );
		add_action( 'woocommerce_cart_item_restored', [ $this, 'cart_item_restored' ], 20 );

		if ( 'yes' === get_option( 'woocommerce_cart_redirect_after_add' ) ) {
			add_filter( 'woocommerce_add_to_cart_redirect', [ $this, 'add_to_cart_redirect' ] );
		}

		add_filter( 'woocommerce_product_add_to_cart_url', [ $this, 'woocommerce_product_link_querystring' ], 10, 2 );
		add_filter( 'woocommerce_loop_product_link', [ $this, 'woocommerce_product_link_querystring' ], 10, 2 );
	}

	/**
	 * Add product object when global variable is missing.
	 *
	 * Ensures the global $product variable is set by checking $GLOBALS['product']
	 * if the global is not available. Used to maintain product context in various hooks.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function add_product_object(): void {
		global $product;

		if ( ! $product && isset( $GLOBALS['product'] ) ) {
			$product = $GLOBALS['product'];
		}
	}

	/**
	 * Add to cart for appointments.
	 *
	 * Outputs the appointment booking form template for adding appointments to cart.
	 * Creates a WC_Appointment_Form instance and renders the appointment form.
	 *
	 * @since 1.0.0
	 *
	 * @return void Outputs HTML template.
	 */
	public function add_to_cart(): void {
		global $product;

		// Prepare form
		$appointment_form = new WC_Appointment_Form( $product );

		// Get template
		wc_get_template(
			'single-product/add-to-cart/appointment.php',
			[
				'appointment_form' => $appointment_form,
			],
			'',
			WC_APPOINTMENTS_TEMPLATE_PATH
		);
	}

	/**
	 * Make appointment quantity in cart readonly.
	 *
	 * Replaces the quantity input field with a hidden input for appointment cart items,
	 * making the quantity read-only since appointment quantities are set during booking.
	 *
	 * @since 1.0.0
	 *
	 * @param string $product_quantity Original quantity HTML.
	 * @param string $cart_item_key    Cart item key.
	 *
	 * @return string Modified quantity HTML with hidden input for appointments, or original HTML for other products.
	 */
	public function cart_item_quantity( string $product_quantity, string $cart_item_key ): string {
		$cart_item = WC()->cart->get_cart_item( $cart_item_key );
		if (! empty( $cart_item['appointment'] ) && ! empty( $cart_item['appointment']['_qty'] )) {
            return sprintf( '%1$s <input type="hidden" name="cart[%2$s][qty]" value="%1$s" />', $cart_item['quantity'], $cart_item_key );
        }

		return $product_quantity;
	}

	/**
	 * Adjust the price of the appointment product based on appointment properties.
	 *
	 * Modifies the cart item price based on appointment cost and quantity.
	 * Sets the product price to the appointment cost if specified in cart item data.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, mixed> $cart_item Cart item array.
	 *
	 * @return array<string, mixed> Modified cart item array with adjusted price.
	 */
	public function add_cart_item( array $cart_item ): array {
		if ( ! empty( $cart_item['appointment'] ) && isset( $cart_item['appointment']['_cost'] ) && '' !== $cart_item['appointment']['_cost'] ) {
			$quantity = isset( $cart_item['appointment']['_qty'] ) && 0 !== $cart_item['appointment']['_qty'] ? $cart_item['appointment']['_qty'] : 1;
			$cart_item['data']->set_price( $cart_item['appointment']['_cost'] / $quantity );
		}

		return $cart_item;
	}

	/**
	 * Get data from the session and add to the cart item's meta.
	 *
	 * Restores appointment data from session when cart is loaded.
	 * Applies appointment pricing adjustments via add_cart_item().
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, mixed> $cart_item     Cart item array.
	 * @param array<string, mixed> $values        Session values containing appointment data.
	 * @param string              $cart_item_key Cart item key.
	 *
	 * @return array<string, mixed> Cart item array with appointment data restored.
	 */
	public function get_cart_item_from_session( $cart_item, array $values, string $cart_item_key ): array {
		if ( ! empty( $values['appointment'] ) ) {
			$cart_item['appointment'] = $values['appointment'];
			$cart_item                = $this->add_cart_item( $cart_item );
		}

		return $cart_item;
	}

	/**
	 * Handle appointment when cart item is removed.
	 *
	 * Schedules deletion of the associated appointment when a cart item is removed.
	 * Prevents immediate deletion to allow cart restoration functionality.
	 *
	 * @since 1.0.0
	 *
	 * @param string $cart_item_key Cart item key identifying which item was removed.
	 *
	 * @return void
	 */
	public function cart_item_removed( $cart_item_key ): void {
		$cart_item = WC()->cart->removed_cart_contents[ $cart_item_key ];

		if ( isset( $cart_item['appointment'] ) ) {
			$appointment_id = $cart_item['appointment']['_appointment_id'];
			$appointment    = get_wc_appointment( $appointment_id );
			if ( $appointment && $appointment->has_status( WC_Appointments_Constants::STATUS_IN_CART ) ) {
				$appointment->update_status( WC_Appointments_Constants::STATUS_WAS_IN_CART );
				WC_Cache_Helper::get_transient_version( 'appointments', true );
				// Normal cart operation - no logging needed
			}
		}
	}

	/**
	 * Restore appointment when cart item is restored.
	 *
	 * Cancels scheduled deletion and restores appointment status to 'in-cart'
	 * when a cart item is restored from removed state.
	 *
	 * @since 1.0.0
	 *
	 * @param string $cart_item_key Cart item key identifying which item was restored.
	 *
	 * @return void
	 */
	public function cart_item_restored( $cart_item_key ): void {
		$cart      = WC()->cart->get_cart();
		$cart_item = $cart[ $cart_item_key ];

		if ( isset( $cart_item['appointment'] ) ) {
			$appointment_id = $cart_item['appointment']['_appointment_id'];
			$appointment    = get_wc_appointment( $appointment_id );
			if ( $appointment && $appointment->has_status( WC_Appointments_Constants::STATUS_WAS_IN_CART ) ) {
				$appointment->update_status( WC_Appointments_Constants::STATUS_IN_CART );
				WC_Cache_Helper::get_transient_version( 'appointments', true );
				$this->schedule_cart_removal( $appointment_id );
				// Normal cart operation - no logging needed
			}
		}
	}

	/**
	 * Schedule appointment to be deleted if inactive.
	 *
	 * Schedules an Action Scheduler event to remove the appointment from cart
	 * if the customer remains inactive for the specified hold stock time.
	 * Uses WooCommerce hold stock minutes setting (default 60 minutes).
	 *
	 * @since 1.0.0
	 *
	 * @param int $appointment_id Appointment ID to schedule removal for.
	 *
	 * @return void
	 */
	public function schedule_cart_removal( $appointment_id ): void {
		$hold_stock_minutes = (int) get_option( 'woocommerce_hold_stock_minutes', 60 );

		// Make sure appointments are cleared, regardless of WC stock options.
		if ( ! $hold_stock_minutes || 0 === $hold_stock_minutes ) {
			$hold_stock_minutes = 60;
		}

		$minutes = apply_filters( 'woocommerce_appointments_remove_inactive_cart_time', $hold_stock_minutes );

		/**
		 * If this has been emptied, or set to 0, it will just exit. This means that in-cart appointments will need to be manually removed.
		 * Also take note that if the $minutes var is set to 5 or less, this means that it is possible for the in-cart appointment to be
		 * removed before the customer is able to check out.
		 */
		if ( empty( $minutes ) ) {
			return;
		}

		$timestamp = time() + MINUTE_IN_SECONDS * (int) $minutes;

		as_schedule_single_action( $timestamp, 'wc-appointment-remove-inactive-cart', [ $appointment_id ], 'wca' );
	}

	/**
	 * Check for invalid appointments when cart is loaded from session.
	 *
	 * @since 1.0.0
	 * @param WC_Cart $cart Cart object.
	 * @return void
	 */
	public function cart_loaded_from_session( $cart ) {
		if ( $cart->is_empty() ) {
			return;
		}

		$removed_titles = [];
		$valid_statuses = [
			'was-in-cart',
			'in-cart',
			'unpaid',
			'paid',
			'pending-confirmation',
			'wc-partial-payment',
			'partial-payment',
		];
		$paid_statuses  = apply_filters( 'woocommerce_appointments_paid_order_statuses', [ 'processing', 'completed', 'wc-partial-payment', 'partial-payment' ] );

		foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) {
			$appointment_id = $cart_item['appointment']['_appointment_id'] ?? 0;

			if ( ! $appointment_id ) {
				continue;
			}

			// Ensure we have fresh data.
			clean_post_cache( $appointment_id );
			$appointment = get_wc_appointment( $appointment_id );

			// Check 1: Existence (Race Condition Protection).
			// If object load failed, check DB directly to prevent accidental removal during race conditions.
			if ( ! $appointment ) {
				global $wpdb;
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
				$exists_in_db = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE ID = %d AND post_type = 'wc_appointment'", $appointment_id ) );
				if ( $exists_in_db ) {
					continue;
				}
			}

			if ( $appointment ) {
				// Check 2: Order Recovery.
				// If appointment is missing order ID but has item ID, try to recover it.
				if ( ! $appointment->get_order_id() && ( $order_item_id = $appointment->get_order_item_id() ) ) {
					$order_id = 0;
					if ( function_exists( 'wc_get_order_id_by_order_item_id' ) ) {
						$order_id = wc_get_order_id_by_order_item_id( $order_item_id );
					} else {
						global $wpdb;
						// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
						$order_id = $wpdb->get_var( $wpdb->prepare( "SELECT order_id FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d", $order_item_id ) );
					}

					if ( $order_id ) {
						$appointment->set_order_id( $order_id );
						$appointment->save();
					}
				}

				// Check 3: Order Status (Already Paid?).
				if ( $appointment->get_order_id() ) {
					$order = wc_get_order( $appointment->get_order_id() );
					if ( $order && in_array( $order->get_status(), $paid_statuses, true ) ) {
						// Already paid, remove from cart silently.
						unset( $cart->cart_contents[ $cart_item_key ] );
					}
					// If order exists (paid or not), we handle it here and skip further checks.
					continue;
				}

				// Check 4: Appointment Status Validity.
				if ( $appointment->has_status( $valid_statuses ) ) {
					continue;
				}
			}

			// Removal: Appointment is invalid or inactive.
			unset( $cart->cart_contents[ $cart_item_key ] );

			if ( ! empty( $cart_item['product_id'] ) ) {
				$removed_titles[] = sprintf( '<a href="%s">%s</a>', get_permalink( $cart_item['product_id'] ), get_the_title( $cart_item['product_id'] ) );
			}
		}

		if ( ! empty( $removed_titles ) ) {
			$cart->calculate_totals();
			$removed_titles = array_unique( $removed_titles );
			$message        = sprintf(
				/* translators: %s: list of product titles */
				_n( 'An appointment for %s has been removed from your cart due to inactivity.', 'Appointments for %s have been removed from your cart due to inactivity.', count( $removed_titles ), 'woocommerce-appointments' ),
				wc_format_list_of_items( $removed_titles )
			);

			if ( ! wc_has_notice( $message, 'notice' ) ) {
				wc_add_notice( $message, 'notice' );
			}
		}
	}

	/**
	 * Add posted data to the cart item.
	 *
	 * Processes appointment form data and adds it to cart item meta.
	 * Calculates appointment cost, creates appointment object, and schedules cart removal.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, mixed> $cart_item_meta Cart item meta array.
	 * @param int                  $product_id     Product ID.
	 *
	 * @return array<string, mixed> Cart item meta array with appointment data added.
	 *
	 * @throws Exception If appointment cost calculation fails or appointment creation fails.
	 */
	public function add_cart_item_data( array $cart_item_meta, int $product_id ): array {
		$product = wc_get_product( $product_id );

		if ( ! is_wc_appointment_product( $product ) ) {
			return $cart_item_meta;
		}

		if ( ! array_key_exists( 'appointment', $cart_item_meta ) ) {
			$cart_item_meta['appointment'] = wc_appointments_get_posted_data( $_POST, $product );
		}
		$cart_item_meta['appointment']['_cost'] = WC_Appointments_Cost_Calculation::calculate_appointment_cost( $_POST, $product );

		if ( $cart_item_meta['appointment']['_cost'] instanceof WP_Error ) {
			throw new Exception( esc_html( $cart_item_meta['appointment']['_cost']->get_error_message() ) );
		}

		// Create the new appointment
		$new_appointment = $this->add_appointment_from_cart_data( $cart_item_meta, $product_id );

		// Handle errors from appointment creation (e.g., double-booking detected).
		if ( is_wp_error( $new_appointment ) ) {
			throw new Exception( $new_appointment->get_error_message() );
		}

		// Store in cart
		$cart_item_meta['appointment']['_appointment_id'] = $new_appointment->get_id();

		// Schedule this item to be removed from the cart if the user is inactive
		$this->schedule_cart_removal( $new_appointment->get_id() );

		return $cart_item_meta;
	}


	/**
	 * Create appointment from cart data.
	 *
	 * Creates a new appointment object from cart item data with booking lock protection.
	 * Uses exponential backoff retry if lock cannot be acquired initially.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, mixed> $cart_item_meta Cart item meta containing appointment data.
	 * @param int                  $product_id     Product ID.
	 * @param string               $status         Optional. Initial appointment status. Default 'in-cart'.
	 *
	 * @return WC_Appointment|WP_Error Appointment object on success, WP_Error on failure (e.g., booking locked).
	 */
	private function add_appointment_from_cart_data( array $cart_item_meta, int $product_id, $status = 'in-cart' ) {
		// Use booking handler for appointment creation from cart data.
		$handler = new WC_Appointment_Booking_Handler();
		return $handler->create_from_cart_data( $cart_item_meta, $product_id, $status );
	}

	/**
	 * Put meta data into format which can be displayed.
	 *
	 * Formats appointment data for display in cart and checkout.
	 * Converts appointment metadata into human-readable key-value pairs.
	 *
	 * @since 1.0.0
	 *
	 * @param array<int, array<string, mixed>> $other_data Existing item data array.
	 * @param array<string, mixed>            $cart_item  Cart item array.
	 *
	 * @return array<int, array<string, mixed>> {
	 *     Array of item data arrays with 'name' and 'value' keys.
	 *
	 *     @type array{
	 *         name: string,
	 *         value: string
	 *     } $item_data Formatted appointment data for display.
	 * }
	 */
	public function get_item_data( $other_data, array $cart_item ): array {
		if ( empty( $cart_item['appointment'] ) ) {
			return $other_data;
		}

		if ( ! empty( $cart_item['appointment']['_appointment_id'] ) ) {
			$appointment = get_wc_appointment( $cart_item['appointment']['_appointment_id'] );
		}

		if ( ! empty( $cart_item['appointment'] ) ) {
			foreach ( $cart_item['appointment'] as $key => $value ) {
				if ( substr( $key, 0, 1 ) !== '_' ) {
					$other_data[] = [
						'name'    => get_wc_appointment_data_label( $key, $cart_item['data'] ),
						'value'   => $value,
						'display' => '',
					];
				}
			}
		}

		return $other_data;
	}

	/**
	 * Add appointment meta data to order item.
	 *
	 * Links appointment to order item when order is created.
	 * Updates appointment with order ID, order item ID, and customer ID.
	 * Handles draft orders specially to maintain 'in-cart' status during checkout block flow.
	 *
	 * @since 1.0.0
	 *
	 * @param int                    $item_id Order item ID.
	 * @param array<string, mixed>|object $values Cart item values (array) or order item object with legacy_values property.
	 *
	 * @return void
	 */
	public function order_item_meta( $item_id, $values ): void {
		$appointment_cost = 0;

		// Handle case where $values is an object (e.g., WC_Order_Item_Tax) instead of array
		if ( ! is_array( $values ) ) {
			// If it's an object with legacy_values, use that, otherwise skip
			if ( ! property_exists( $values, 'legacy_values' ) || empty( $values->legacy_values ) || ! is_array( $values->legacy_values ) ) {
				return;
			}
			$values = $values->legacy_values;
		}

		if ( ! empty( $values['appointment'] ) ) {
			$product          = $values['data'];
			$appointment_id   = $values['appointment']['_appointment_id'];
			$appointment_cost = (float) $values['appointment']['_cost'];
		}

		if ( isset( $appointment_id ) ) {
			$appointment = get_wc_appointment( $appointment_id );

			// Validate appointment object exists and is valid.
			if ( ! $appointment || ! is_a( $appointment, 'WC_Appointment' ) ) {
				if ( function_exists( 'wc_add_appointment_log' ) ) {
					wc_add_appointment_log( $this->id, sprintf( 'Failed to load appointment #%d for order item #%d', $appointment_id, $item_id ) );
				}
				return;
			}

			if ( function_exists( 'wc_get_order_id_by_order_item_id' ) ) {
				$order_id = wc_get_order_id_by_order_item_id( $item_id );
			}
			if ( empty( $order_id ) ) {
				global $wpdb;
				$order_id = (int) $wpdb->get_var(
					$wpdb->prepare(
						"SELECT order_id FROM {$wpdb->prefix}woocommerce_order_items WHERE order_item_id = %d",
						$item_id
					)
				);
			}

			// Validate order exists.
			if ( empty( $order_id ) ) {
				if ( function_exists( 'wc_add_appointment_log' ) ) {
					wc_add_appointment_log( $this->id, sprintf( 'Failed to get order ID for order item #%d', $item_id ) );
				}
				return;
			}

			$order = wc_get_order( $order_id );

			// Validate order object exists and is valid.
			if ( ! $order || ! is_a( $order, 'WC_Order' ) ) {
				if ( function_exists( 'wc_add_appointment_log' ) ) {
					wc_add_appointment_log( $this->id, sprintf( 'Failed to load order #%d for appointment #%d', $order_id, $appointment_id ) );
				}
				return;
			}

			$order_status = $order->get_status();

			// Testing.
			#error_log( var_export( 'Appointment ID', true ) );
			#error_log( var_export( $appointment_id, true ) );
			#error_log( var_export( 'Appointment Cost', true ) );
			#error_log( var_export( $appointment_cost, true ) );
			#error_log( var_export( 'Appointment Status', true ) );
			#error_log( var_export( $appointment->get_status(), true ) );
			#error_log( var_export( 'Order Status', true ) );
			#error_log( var_export( $order_status, true ) );

			$appointment->set_order_id( $order_id );
			$appointment->set_order_item_id( $item_id );
			if ( ! $appointment->get_customer_id() && $order->get_customer_id() ) {
				$appointment->set_customer_id( $order->get_customer_id() );
			}

			/**
			 * In this particular case, the status will be 'in-cart' as we don't want to change it
			 * before the actual order is done if we're dealing with the checkout blocks.
			 * The checkout block creates a draft order before it is then changes to another more final status.
			 * Later the woocommerce_blocks_checkout_order_processed hook is called and
			 * review_items_on_checkout runs to change the status of the appointment to their correct value.
			 */
			// For draft orders, keep appointments in a draft-like state unless they were removed.
			// If the appointment was already marked as 'was-in-cart' (removed), do not revert it.
			if ( 'checkout-draft' === $order_status && ! $appointment->has_status( WC_Appointments_Constants::STATUS_WAS_IN_CART ) ) {
				// Maintain 'in-cart' to avoid premature status changes during draft lifecycle.
				$appointment->set_status( WC_Appointments_Constants::STATUS_IN_CART );
			}

			// Save appointment with error handling and retry logic.
			$saved_appointment_id = null;
			$max_retries = 3;
			$retry_count = 0;

			while ( $retry_count < $max_retries ) {
				try {
					$saved_appointment_id = $appointment->save();

					// Verify the appointment was actually saved by checking if ID exists.
					if ( $saved_appointment_id && $saved_appointment_id > 0 ) {
						// Double-check the appointment exists in database.
						$verify_appointment = get_wc_appointment( $saved_appointment_id );
						if ( $verify_appointment && is_a( $verify_appointment, 'WC_Appointment' ) ) {
							// Successfully saved and verified.
							wc_update_order_item_meta( $item_id, '_appointment_id', [ $saved_appointment_id ] );
							break;
						}
					}

					// If we get here, save didn't work properly.
					$retry_count++;
					if ( $retry_count < $max_retries ) {
						// Wait a brief moment before retrying (helps with race conditions).
						usleep( 100000 ); // 100ms
					}
				} catch ( Exception $e ) {
					$retry_count++;
					if ( function_exists( 'wc_add_appointment_log' ) ) {
						wc_add_appointment_log( $this->id, sprintf( 'Exception saving appointment #%d for order #%d (attempt %d/%d): %s', $appointment_id, $order_id, $retry_count, $max_retries, $e->getMessage() ) );
					}

					if ( $retry_count < $max_retries ) {
						// Wait before retrying.
						usleep( 100000 ); // 100ms
					} else {
						// Final attempt failed, log error and add order note.
						if ( function_exists( 'wc_add_appointment_log' ) ) {
							wc_add_appointment_log( $this->id, sprintf( 'CRITICAL: Failed to save appointment #%d for order #%d after %d attempts', $appointment_id, $order_id, $max_retries ) );
						}
						$order->add_order_note( sprintf( __( 'Warning: Failed to save appointment #%d. Please check appointment manually.', 'woocommerce-appointments' ), $appointment_id ) );
					}
				}
			}

			// If save still failed after retries, try one more time with a fresh appointment object.
			if ( ! $saved_appointment_id || $saved_appointment_id <= 0 ) {
				try {
					// Reload appointment to get fresh state.
					$fresh_appointment = get_wc_appointment( $appointment_id );
					if ( $fresh_appointment && is_a( $fresh_appointment, 'WC_Appointment' ) ) {
						$fresh_appointment->set_order_id( $order_id );
						$fresh_appointment->set_order_item_id( $item_id );
						if ( ! $fresh_appointment->get_customer_id() && $order->get_customer_id() ) {
							$fresh_appointment->set_customer_id( $order->get_customer_id() );
						}
						if ( 'checkout-draft' === $order_status && ! $fresh_appointment->has_status( WC_Appointments_Constants::STATUS_WAS_IN_CART ) ) {
							$fresh_appointment->set_status( WC_Appointments_Constants::STATUS_IN_CART );
						}

						$saved_appointment_id = $fresh_appointment->save();
						if ( $saved_appointment_id && $saved_appointment_id > 0 ) {
							wc_update_order_item_meta( $item_id, '_appointment_id', [ $saved_appointment_id ] );
							// Success - no logging needed for normal operations
						}
					}
				} catch ( Exception $e ) {
					if ( function_exists( 'wc_add_appointment_log' ) ) {
						wc_add_appointment_log( $this->id, sprintf( 'CRITICAL: Final retry failed for appointment #%d: %s', $appointment_id, $e->getMessage() ) );
					}
					$order->add_order_note( sprintf( __( 'CRITICAL: Failed to save appointment #%d after all retry attempts. Manual intervention required.', 'woocommerce-appointments' ), $appointment_id ) );
				}
			}
		}
	}

	/**
	 * Redirects directly to the cart for products that need confirmation.
	 *
	 * Redirects users to the cart page when adding appointment products that require confirmation,
	 * instead of redirecting to the product page. Clears success notices to avoid confusion.
	 *
	 * @since 1.0.0
	 * @version 3.4.0
	 *
	 * @param string $url Original redirect URL.
	 *
	 * @return string Cart URL if product requires confirmation, otherwise original URL.
	 */
	public function add_to_cart_redirect( string $url ): string {
		if ( isset( $_REQUEST['add-to-cart'] ) && is_numeric( $_REQUEST['add-to-cart'] ) && wc_appointment_requires_confirmation( intval( $_REQUEST['add-to-cart'] ) ) ) {
			// Remove add to cart messages only in case there's no error.
			$notices = wc_get_notices();
			if ( empty( $notices['error'] ) ) {
				wc_clear_notices();

				// Go to checkout.
				return wc_get_cart_url();
			}
		}

		return $url;
	}

	/**
	 * Add querystring to product link.
	 *
	 * Preserves appointment booking parameters (date, time, staff) in product permalinks
	 * when navigating from appointment calendar or other booking interfaces.
	 *
	 * @since 3.4.0
	 *
	 * @param string              $permalink Product permalink.
	 * @param WC_Product|object  $product   Product object.
	 *
	 * @return string Product permalink with appointment query parameters added.
	 */
	public function woocommerce_product_link_querystring( $permalink, $product ) {
		if ( ! is_wc_appointment_product( $product ) ) {
			return $permalink;
		}

		// Querystrings exist?
		$date  = isset( $_GET['min_date'] ) ? wc_clean( wp_unslash( $_GET['min_date'] ) ) : ''; // WPCS: input var ok, CSRF ok.
		$time  = isset( $_GET['time'] ) ? wc_clean( wp_unslash( $_GET['time'] ) ) : ''; // WPCS: input var ok, CSRF ok.
		$staff = isset( $_GET['staff'] ) ? wc_clean( wp_unslash( $_GET['staff'] ) ) : ''; // WPCS: input var ok, CSRF ok.

		if ( $date ) {
			$permalink = add_query_arg( 'date', $date, $permalink );
		}
		if ( $time ) {
			$permalink = add_query_arg( 'time', $time, $permalink );
		}
		if ( $staff ) {
			$permalink = add_query_arg( 'staff', $staff, $permalink );
		}

		return apply_filters( 'woocommerce_appointment_get_permalink', $permalink, $this );
	}

	/**
	 * Remove all appointments that require confirmation from cart.
	 *
	 * Removes appointment products that require confirmation from the cart.
	 * Used when adding a non-confirmation product to maintain cart compatibility.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	protected function remove_appointment_that_requires_confirmation() {
		foreach ( WC()->cart->cart_contents as $item_key => $item ) {
			if ( wc_appointment_requires_confirmation( $item['product_id'] ) ) {
				WC()->cart->set_quantity( $item_key, 0 );
			}
		}
	}

	/**
	 * Review appointment items on block checkout.
	 *
	 * Updates appointment statuses when order is processed via WooCommerce Blocks checkout.
	 * Handles status transitions from 'in-cart' to appropriate status based on product requirements.
	 *
	 * @since 1.0.0
	 *
	 * @param WC_Order $order Order object.
	 *
	 * @return void
	 */
	public function review_items_on_block_checkout( $order ): void {
		$order_id = $order->get_id();

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

		if ( class_exists( 'WC_Deposits' ) || class_exists( 'Webtomizer\WCDP\WC_Deposits' ) ) {
			// Is this a final payment?
			$parent_id = wp_get_post_parent_id( $order_id );
			if ( ! empty( $parent_id ) ) {
				$order_id = $parent_id;
			}
		}

		$order        = wc_get_order( $order_id );
		$order_status = $order->get_status();

		$appointments = WC_Appointment_Data_Store::get_appointment_ids_from_order_id( $order_id );

		// Fallback: Check for order items that should have appointments but don't.
		$this->ensure_appointments_for_order_items( $order );

		foreach ( $appointments as $appointment_id ) {
			$appointment = get_wc_appointment( $appointment_id );

			// Skip if appointment doesn't exist or is invalid.
			if ( ! $appointment || ! is_a( $appointment, 'WC_Appointment' ) ) {
				if ( function_exists( 'wc_add_appointment_log' ) ) {
					wc_add_appointment_log( $this->id, sprintf( 'Invalid appointment #%d found for order #%d', $appointment_id, $order_id ) );
				}
				continue;
			}

			$product_id  = $appointment->get_product_id();
			if ( $product_id === 0 ) {
				continue;
			}

			// Skip appointments that were removed from cart.
			if ( $appointment->has_status( WC_Appointments_Constants::STATUS_WAS_IN_CART ) ) {
				continue;
			}

			/**
			 * We just want to deal with the appointments that we left forcibly on the 'in-cart' state
			 * and provide them the same state they would be if not using blocks.
			 */
			if ( ! wc_appointment_requires_confirmation( $product_id ) && ! in_array( $order_status, [ 'processing', 'completed' ], true ) ) {
				/**
				 * We need to bring the appointment status from the new in-cart status to unpaid if it doesn't require confirmation
				 */
				$appointment->set_status( 'unpaid' );
				$saved_id = $appointment->save();
				if ( (! $saved_id || $saved_id <= 0) && function_exists( 'wc_add_appointment_log' ) ) {
					wc_add_appointment_log( $this->id, sprintf( 'Failed to save appointment #%d status update for order #%d', $appointment_id, $order_id ) );
				}
			} elseif ( 'in-cart' === $appointment->get_status() && wc_appointment_requires_confirmation( $product_id ) ) {
				/**
				 * If the order is in cart and requires confirmation, we need to change this.
				 */
				$appointment->set_status( 'pending-confirmation' );
				$saved_id = $appointment->save();
				if ( (! $saved_id || $saved_id <= 0) && function_exists( 'wc_add_appointment_log' ) ) {
					wc_add_appointment_log( $this->id, sprintf( 'Failed to save appointment #%d status update for order #%d', $appointment_id, $order_id ) );
				}
			}
		}
	}

	/**
	 * Review appointment items on shortcode checkout.
	 *
	 * Updates appointment statuses when order is processed via shortcode checkout.
	 * Handles status transitions from 'in-cart' to appropriate status based on product requirements.
	 * Skips draft orders to avoid premature status changes.
	 *
	 * @since 1.0.0
	 *
	 * @param int $order_id Order ID.
	 *
	 * @return void
	 */
	public function review_items_on_shortcode_checkout( $order_id ): void {
		if ( empty( $order_id ) ) {
			return;
		}

		if ( class_exists( 'WC_Deposits' ) || class_exists( 'Webtomizer\WCDP\WC_Deposits' ) ) {
			// Is this a final payment?
			$parent_id = wp_get_post_parent_id( $order_id );
			if ( ! empty( $parent_id ) ) {
				$order_id = $parent_id;
			}
		}

		/**
		 * We need to make sure we don't do anything to the appointment just yet because of the new checkout-draft status
		 * assigned by the checkout block when entering the checkout page.
		 */
		$order        = wc_get_order( $order_id );
		$order_status = $order->get_status();

		if ( 'checkout-draft' === $order_status ) {
			return;
		}

		$appointments = WC_Appointment_Data_Store::get_appointment_ids_from_order_id( $order_id );

		// Fallback: Check for order items that should have appointments but don't.
		$this->ensure_appointments_for_order_items( $order );

		foreach ( $appointments as $appointment_id ) {
			$appointment = get_wc_appointment( $appointment_id );

			// Skip if appointment doesn't exist or is invalid.
			if ( ! $appointment || ! is_a( $appointment, 'WC_Appointment' ) ) {
				if ( function_exists( 'wc_add_appointment_log' ) ) {
					wc_add_appointment_log( $this->id, sprintf( 'Invalid appointment #%d found for order #%d', $appointment_id, $order_id ) );
				}
				continue;
			}

			$product_id  = $appointment->get_product_id();

			if ( $product_id === 0 ) {
				continue;
			}

			// Skip appointments that were removed from cart.
			if ( $appointment->has_status( WC_Appointments_Constants::STATUS_WAS_IN_CART ) ) {
				continue;
			}

			if ( ! wc_appointment_requires_confirmation( $product_id ) && ! in_array( $order_status, [ 'processing', 'completed' ], true ) ) {
				/**
				 * We need to bring the appointment status from the new in-cart status to unpaid if it doesn't require confirmation
				 */
				$appointment->set_status( 'unpaid' );
				$saved_id = $appointment->save();
				if ( (! $saved_id || $saved_id <= 0) && function_exists( 'wc_add_appointment_log' ) ) {
					wc_add_appointment_log( $this->id, sprintf( 'Failed to save appointment #%d status update for order #%d', $appointment_id, $order_id ) );
				}
			} elseif ( 'in-cart' === $appointment->get_status() && wc_appointment_requires_confirmation( $product_id ) ) {
				/**
				 * If the order is in cart and requires confirmation, we need to change this.
				 */
				$appointment->set_status( 'pending-confirmation' );
				$saved_id = $appointment->save();
				if ( (! $saved_id || $saved_id <= 0) && function_exists( 'wc_add_appointment_log' ) ) {
					wc_add_appointment_log( $this->id, sprintf( 'Failed to save appointment #%d status update for order #%d', $appointment_id, $order_id ) );
				}
			}
		}
	}

	/**
	 * When an appointment is added to the cart, validate it.
	 *
	 * Validates the appointment data posted from the product page.
	 * Checks if the selected slots are still available and valid.
	 *
	 * @since 1.0.0
	 *
	 * @param bool $passed     Whether validation passed so far.
	 * @param int  $product_id Product ID being added to cart.
	 * @param int  $qty        Quantity being added.
	 *
	 * @return bool True if validation passes, false otherwise (adds error notice).
	 */
	public function validate_appointment_posted_data( $passed, $product_id, $qty ) {
		$product = wc_get_product( $product_id );

		if ( ! is_wc_appointment_product( $product ) ) {
			return $passed;
		}

		$data     = wc_appointments_get_posted_data( $_POST, $product );
		$validate = $product->is_appointable( $data );

		if ( is_wp_error( $validate ) ) {
			wc_add_notice( $validate->get_error_message(), 'error' );
			return false;
		}

		return $passed;
	}

	/**
	 * Validate cart compatibility with confirmation requirements.
	 *
	 * Ensures cart cannot contain both appointment products that require confirmation
	 * and those that don't. Removes incompatible items and displays notices.
	 *
	 * @since 1.0.0
	 *
	 * @param bool $passed    Whether validation passed so far.
	 * @param int  $product_id Product ID being added to cart.
	 *
	 * @return bool True if validation passes, false if items were removed (adds notice).
	 */
	public function validate_appointment_requires_confirmation( $passed, $product_id ) {
		if ( wc_appointment_requires_confirmation( $product_id ) ) {
			$items = WC()->cart->get_cart();

			foreach ( $items as $item_key => $item ) {
				if ( ! isset( $item['appointment'] ) || ! wc_appointment_requires_confirmation( $item['product_id'] ) ) {
					WC()->cart->remove_cart_item( $item_key );
					// Item qty.
					$item_qty = $item['quantity'] > 1 ? absint( $item['quantity'] ) . ' &times; ' : '';
					// Item name.
					$item_name = apply_filters(
						'woocommerce_add_to_cart_item_name_in_quotes',
						sprintf(
							'&ldquo;%s&rdquo;',
							wp_strip_all_tags( get_the_title( $item['product_id'] ) )
						),
						$item['product_id']
					);
					// Add notice.
					wc_add_notice(
						sprintf(
							/* translators: %s: product name */
							__( '%s has been removed from your cart. It is not possible to complete the purchase along with an appointment that requires confirmation.', 'woocommerce-appointments' ),
							$item_qty . $item_name
						),
						'notice'
					);
				}
			}
		} elseif ( wc_appointment_cart_requires_confirmation() ) {
			// Remove appointment that requires confirmation.
			$this->remove_appointment_that_requires_confirmation();
			// Add notice.
			wc_add_notice( __( 'An appointment that requires confirmation has been removed from your cart. It is not possible to complete the purchase along with an appointment that doesn\'t require confirmation.', 'woocommerce-appointments' ), 'notice' );
		}

		return $passed;
	}

	/**
	 * Disable quantity field editing for appointments in cart block.
	 *
	 * Makes appointment quantities read-only in WooCommerce Blocks cart.
	 * Appointment quantities are set during booking and cannot be changed in cart.
	 *
	 * @since 1.0.0
	 *
	 * @param bool        $value     Whether quantity field is editable.
	 * @param WC_Product  $product   Product object.
	 * @param array<string, mixed> $cart_item Cart item array.
	 *
	 * @return bool False if product is an appointment product, otherwise original value.
	 */
	public function disable_cart_block_qty_field( $value, $product, $cart_item ) {
		#error_log( var_export( $value, true ) );
		#error_log( var_export( $product, true ) );
		#error_log( var_export( $cart_item, true ) );

		// Disable quantity field editing for appointments.
		if ( is_wc_appointment_product( $product ) ) {
			return false;
		}

		return $value;
	}

	/**
	 * Validate sold individually requirement for appointments.
	 *
	 * Ensures appointment products marked as sold individually can only be added once to cart.
	 * Prevents duplicate appointments of the same product in cart.
	 *
	 * @since 1.0.0
	 *
	 * @param bool $passed    Whether validation passed so far.
	 * @param int  $product_id Product ID being added to cart.
	 * @param int  $qty       Quantity being added.
	 *
	 * @return bool True if validation passes, false if product already in cart (adds error notice).
	 */
	public function validate_appointment_sold_individually( $passed, $product_id, $qty ) {
		if ( wc_appointment_sold_individually( $product_id ) ) {
			$product = wc_get_product( $product_id );
			$items   = WC()->cart->get_cart();

			foreach ( $items as $item ) {
				// Ptroduct already in cart. Stop here.
				if ( $item['product_id'] === $product_id ) {
					// Product name.
					$product_name = apply_filters(
						'woocommerce_add_to_cart_item_name_in_quotes',
						sprintf(
							'&ldquo;%s&rdquo;',
							wp_strip_all_tags( get_the_title( $item['product_id'] ) )
						),
						$product->get_name()
					);
					// Add notice.
					$message = sprintf(
						/* translators: %s: product name */
						__( 'You cannot add another %s to your cart.', 'woocommerce-appointments' ),
						$product_name
					);
					$wp_button_class = wc_wp_theme_get_element_class_name( 'button' ) ? ' ' . wc_wp_theme_get_element_class_name( 'button' ) : '';
					wc_add_notice(
						sprintf(
							'<a href="%s" class="button wc-forward%s">%s</a> %s',
							wc_get_cart_url(),
							esc_attr( $wp_button_class ),
							__( 'View cart', 'woocommerce-appointments' ),
							$message
						),
						'error'
					);

					$passed = false;
				}
			}
		}

		return $passed;
	}

	/**
     * Validate appointment product order for checkout block.
     *
     * Wrapper method for WooCommerce Blocks checkout validation.
     * Skips validation if called from admin (Gutenberg editor preview).
     *
     * @since 1.15.76
     *
     * @param \WP_Error $errors WP_Error object to add validation errors to.
     * @param \WC_Cart  $cart   Cart object.
     *
     * @return void
     */
    public function validate_appointment_order_checkout_block_support( \WP_Error $errors, \WC_Cart $cart ): void {
		// Existing checkout validation if rest api request generates from gutenberg editor.
		if ( is_admin() ) {
			return;
		}

		$this->validate_appointment_order( $errors, $cart );
	}

	/**
     * Validate appointment product order for legacy checkout.
     *
     * Wrapper method for legacy shortcode checkout validation.
     * Delegates to validate_appointment_order() with current cart.
     *
     * @since 1.15.76
     *
     * @param array<string, mixed> $data   Checkout data (unused, kept for hook compatibility).
     * @param \WP_Error            $errors WP_Error object to add validation errors to.
     *
     * @return void
     */
    public function validate_appointment_order_legacy_checkout( array $data, \WP_Error $errors ): void {
		$this->validate_appointment_order( $errors, WC()->cart );
	}

	/**
	 * Ensures appointments are saved for all order items that should have them.
	 * This is a fallback mechanism to catch appointments that weren't saved during checkout.
	 *
	 * @param WC_Order $order The order to check.
	 * @return void
	 */
	protected function ensure_appointments_for_order_items( $order ) {
		if ( ! $order || ! is_a( $order, 'WC_Order' ) ) {
			return;
		}

		$order_id = $order->get_id();
		if ( empty( $order_id ) ) {
			return;
		}

		// Get all appointments already linked to this order.
		$existing_appointments = WC_Appointment_Data_Store::get_appointment_ids_from_order_id( $order_id );
		$existing_appointment_item_ids = [];

		// Build a map of order item IDs that already have appointments.
		foreach ( $existing_appointments as $appointment_id ) {
			$appointment = get_wc_appointment( $appointment_id );
			if ( $appointment && is_a( $appointment, 'WC_Appointment' ) ) {
				$item_id = $appointment->get_order_item_id();
				if ( $item_id !== 0 ) {
					$existing_appointment_item_ids[] = $item_id;
				}
			}
		}

		// Check each order item for missing appointments.
		foreach ( $order->get_items() as $item_id => $item ) {
			// Skip if this item already has an appointment.
			if ( in_array( $item_id, $existing_appointment_item_ids, true ) ) {
				continue;
			}

			// Check if this is an appointment product.
			$product = $item->get_product();
            if (! $product) {
                continue;
            }
            if (! is_wc_appointment_product( $product )) {
                continue;
            }

			// Try to get appointment ID from order item meta.
			$appointment_ids = WC_Appointment_Data_Store::get_appointment_ids_from_order_and_item_id( $order_id, $item_id );
			if ( ! empty( $appointment_ids ) ) {
				// Appointment IDs exist in meta, verify they're actually saved.
				foreach ( $appointment_ids as $appointment_id ) {
					$appointment = get_wc_appointment( $appointment_id );
					if ( $appointment && is_a( $appointment, 'WC_Appointment' ) ) {
						// Appointment exists, ensure it's properly linked.
						if ( $appointment->get_order_id() !== $order_id ) {
							$appointment->set_order_id( $order_id );
							$appointment->set_order_item_id( $item_id );
							if ( ! $appointment->get_customer_id() && $order->get_customer_id() ) {
								$appointment->set_customer_id( $order->get_customer_id() );
							}
							$saved_id = $appointment->save();
							if ($saved_id && $saved_id > 0) {
                                // Successfully fixed linkage - no logging needed for normal operations
                            } elseif (function_exists( 'wc_add_appointment_log' )) {
                                // Log if the fix attempt failed
                                wc_add_appointment_log( $this->id, sprintf( 'Failed to fix appointment #%d linkage for order #%d item #%d', $appointment_id, $order_id, $item_id ) );
                            }
						}
					} else {
						// Appointment ID in meta but appointment doesn't exist - remove from meta.
						wc_delete_order_item_meta( $item_id, '_appointment_id' );
						if ( function_exists( 'wc_add_appointment_log' ) ) {
							wc_add_appointment_log( $this->id, sprintf( 'Removed invalid appointment ID #%d from order #%d item #%d meta', $appointment_id, $order_id, $item_id ) );
						}
					}
				}
				continue;
			}

			// No appointment found - try to recover from cart session data if available.
			// This is a last resort fallback - normally appointments should be created during add_to_cart.
			// We can't recreate appointments here without the original cart data, so we'll just log it.
			if ( function_exists( 'wc_add_appointment_log' ) ) {
				wc_add_appointment_log( $this->id, sprintf( 'WARNING: Order #%d item #%d (product #%d) should have an appointment but none found. This may indicate a checkout issue.', $order_id, $item_id, $product->get_id() ) );
			}

			// Add order note for admin visibility.
			$order->add_order_note( sprintf( __( 'Warning: Order item #%d (product: %s) should have an appointment but none was found. Please check manually.', 'woocommerce-appointments' ), $item_id, $product->get_name() ) );
		}
	}

	/**
     * Validate appointment order items during checkout.
     *
     * Validates appointment availability for all appointments in cart during checkout.
     * Excludes in-cart appointments from availability checks and flags checkout appointments
     * as temporarily confirmed to prevent double-booking validation issues.
     *
     * @since 1.15.76
     *
     * @param \WP_Error $errors WP_Error object to add validation errors to.
     * @param \WC_Cart  $cart   Cart object containing appointment items.
     *
     * @return void
     */
    public function validate_appointment_order( \WP_Error $errors, \WC_Cart $cart ): void {
		// Do not need to validate if cart is empty.
		if ( $cart->is_empty() ) {
			return;
		}

		$cart_items                             = $cart->get_cart();
		$temporary_confirmed_order_appointments = [];

		$appointment_errors = [];

		foreach ( $cart_items as $product_data ) {
			/* @var WC_Product_Booking $product */
			$product = $product_data['data'];

			if ( ! is_wc_appointment_product( $product ) ) {
				continue;
			}

			$appointment = new WC_Appointment( $product_data['appointment']['_appointment_id'] );

			// Unique key to store temporary confirmed appointments in array.
			// Each appointment has following unique key: appointment_id + staff_id + start_date + end_date.
			$temporary_confirmed_checkout_appointments_array_key = "{$appointment->get_product_id()}_{$appointment->get_staff_ids()}_{$appointment->get_start()}_{$appointment->get_end()}";

			if ( array_key_exists( $temporary_confirmed_checkout_appointments_array_key, $temporary_confirmed_order_appointments ) ) {
				$product->confirmed_order_appointments[] = $temporary_confirmed_order_appointments[ $temporary_confirmed_checkout_appointments_array_key ];
			}

			$product->check_in_cart = false;
			$validate               = $product->is_appointable( $product_data['appointment'] );

			if ( is_wp_error( $validate ) ) {
				$appointment_errors[ "appointment-order-item-error-{$appointment->get_product_id()}" ] = sprintf(
					/* translators: 1: Booking product name */
					esc_html__(
						'Sorry, the selected block is no longer available for %1$s. Please choose another block.',
						'woocommerce-appointments'
					),
					$product->get_name()
				);
			}

			// Flag appointment as temporary confirmed for availability check.
			$temporary_confirmed_order_appointments[ $temporary_confirmed_checkout_appointments_array_key ] = $appointment;
		}

		// Add appointment checkout errors.
		foreach ( $appointment_errors as $error_code => $error_message ) {
				$errors->add( $error_code, $error_message );
			}
	}
}

$GLOBALS['wc_appointment_cart_manager'] = new WC_Appointment_Cart_Manager();
