<?php
/**
 * Handles order status transitions and keeps bookings in sync.
 *
 * @package WooCommerce Bookings
 */

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

/**
 * WC_Booking_Order_Manager class.
 *
 * Handles order status transitions and keeps bookings in sync.
 *
 * @version 3.5.2
 */
class WC_Booking_Order_Manager {

	/**
	 * ID being synced.
	 *
	 * @var array
	 */
	private static $syncing_ids = array();

	/**
	 * Instance.
	 *
	 * @var WC_Booking_Order_Manager
	 */
	private static $instance;

	/**
	 * Get the instance.
	 *
	 * @return WC_Booking_Order_Manager
	 */
	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Constructor sets up actions
	 */
	public function __construct() {
		add_action( 'woocommerce_order_item_meta_end', array( $this, 'booking_display' ), 10, 3 );
		// WooCommerce PIP compatibility
		add_action( 'wc_pip_order_item_meta_end', array( $this, 'booking_display' ), 10, 3 );

		// Add a "My Bookings" area to the My Account page
		add_action( 'init', array( $this, 'add_endpoint' ) );
		add_action( 'init', array( $this, 'register_endpoint_hooks' ) );
		add_filter( 'woocommerce_settings_pages', array( $this, 'add_endpoint_account_settings' ) );
		add_filter( 'query_vars', array( $this, 'add_query_vars' ), 0 );
		add_filter( 'the_title', array( $this, 'endpoint_title' ) );
		add_filter( 'woocommerce_get_query_vars', array( $this, 'add_bookings_woocommerce_query_vars' ) );
		add_filter( 'woocommerce_account_menu_items', array( $this, 'my_account_menu_item' ) );

		// Complete booking orders if virtual
		add_filter( 'woocommerce_payment_complete_order_status', array( $this, 'complete_order' ), 20, 3 );

		// When an order is processed or completed, we can mark publish the pending bookings
		add_action( 'woocommerce_order_status_processing', array( $this, 'publish_bookings' ), 10, 1 );
		add_action( 'woocommerce_order_status_completed', array( $this, 'publish_bookings' ), 10, 1 );

		// When an order goes from "Completed" or "Processing" to "Pending Payment", mark bookings as "Unpaid".
		add_action( 'woocommerce_order_status_processing_to_pending', array( $this, 'mark_as_unpaid_bookings' ), 10, 1 );
		add_action( 'woocommerce_order_status_completed_to_pending', array( $this, 'mark_as_unpaid_bookings' ), 10, 1 );

		// When an order is cancelled/fully refunded, cancel the bookings
		add_action( 'woocommerce_order_status_cancelled', array( $this, 'cancel_bookings' ), 10, 1 );
		add_action( 'woocommerce_order_status_refunded', array( $this, 'cancel_bookings' ), 10, 1 );
		add_action( 'woocommerce_order_partially_refunded', array( $this, 'cancel_bookings_for_partial_refunds' ), 10, 1 );

		// Status transitions
		add_action( 'before_delete_post', array( $this, 'delete_post' ) );
		add_action( 'wp_trash_post', array( $this, 'trash_post' ) );
		add_action( 'untrash_post', array( $this, 'untrash_post' ) );
		add_action( 'wp_untrash_post_status', array( $this, 'untrash_post_status' ), 10, 3 );

		// Handle HPOS order trash and delete.
		add_action( 'woocommerce_before_delete_order', array( $this, 'delete_post' ) );
		add_action( 'woocommerce_before_trash_order', array( $this, 'trash_post' ) );
		add_action( 'woocommerce_untrash_order', array( $this, 'untrash_post' ) );

		add_action( 'woocommerce_booking_cancelled', array( $this, 'maybe_cancel_order' ) );
		add_action( 'woocommerce_booking_cancelled', array( $this, 'track_cancel_timestamp' ) );
		add_action( 'woocommerce_booking_cancelled', array( $this, 'mark_as_unattended' ) );
		add_action( 'woocommerce_booking_paid', array( $this, 'maybe_process_order' ) );
		add_action( 'woocommerce_booking_unpaid', array( $this, 'maybe_pending_order' ) );
		add_action( 'woocommerce_booking_complete', array( $this, 'maybe_set_as_attended' ), 10, 2 );

		// Prevent pending being cancelled
		add_filter( 'woocommerce_cancel_unpaid_order', array( $this, 'prevent_cancel' ), 10, 2 );

		// Control the my orders actions.
		add_filter( 'woocommerce_my_account_my_orders_actions', array( $this, 'my_orders_actions' ), 10, 2 );

		// Sync order user with booking user
		add_action( 'woocommerce_booking_in-cart_to_unpaid', array( $this, 'attach_new_user' ), 10, 2 );
		add_action( 'woocommerce_booking_in-cart_to_pending-confirmation', array( $this, 'attach_new_user' ), 10, 2 );

		add_action( 'woocommerce_before_order_object_save', array( $this, 'sync_booking_customer_id' ), 10, 1 );

		// Resolve WooCommerce Analytics customer ID on every booking save.
		add_action( 'woocommerce_before_booking_object_save', array( $this, 'sync_woo_customer_id_on_booking_save' ) );

		// Backfill woo_customer_id on bookings after WooCommerce Analytics syncs the order's customer row.
		add_action( 'woocommerce_analytics_update_order_stats', array( $this, 'backfill_woo_customer_id_for_order' ) );

		// Failed Order Management.
		add_action( 'woocommerce_order_status_failed', array( $this, 'schedule_failed_order_event' ) );
		add_action( 'woocommerce_bookings_failed_order_expired', array( $this, 'handle_failed_order_scheduled_event' ), 20, 2 );

		// My account Bookings section scripts.
		add_action( 'wp_enqueue_scripts', array( $this, 'my_account_scripts' ) );

		// Add custom HTML after Booking table in the email.
		add_action( 'woocommerce_email_after_order_table', array( $this, 'add_admin_calender_link' ), 10, 2 );

		// Format booking ID meta. To be removed if we switch the format of the booking ID to int instead of array.
		add_filter( 'woocommerce_order_item_get_formatted_meta_data', array( $this, 'order_item_get_formatted_meta_data' ), 10, 2 );
	}

	/**
	 * Show booking data if a line item is linked to a booking ID.
	 */
	public function booking_display( $item_id, $item, $order ) {
		$booking_ids = WC_Booking_Data_Store::get_booking_ids_from_order_item_id( $item_id );

		wc_get_template(
			'order/booking-display.php',
			array(
				'booking_ids'       => $booking_ids,
				'endpoint'          => $this->get_endpoint(),
				'hide_item_details' => true,
			),
			'woocommerce-bookings',
			WC_BOOKINGS_TEMPLATE_PATH
		);
	}

	/**
	 * Is ID being synced?
	 */
	private static function is_syncing( $id ) {
		return in_array( $id, self::$syncing_ids );
	}

	/**
	 * Store ID on sync.
	 */
	private static function syncing_start( $id ) {
		self::$syncing_ids[] = $id;
	}

	/**
	 * Remove ID on sync completion.
	 */
	private static function syncing_stop( $id ) {
		self::$syncing_ids = array_diff( self::$syncing_ids, array( $id ) );
	}

	/**
	 * Return the my-account page endpoint.
	 *
	 * @since 1.9.11
	 * @return string
	 */
	public function get_endpoint() {
		$endpoint = get_option( 'woocommerce_myaccount_bookings_endpoint', 'bookings' );
		/**
		 * Filter the my-account page endpoint slug for bookings.
		 *
		 * @since 3.6.0
		 */
		return apply_filters( 'woocommerce_bookings_account_endpoint', $endpoint );
	}

	/**
	 * Add the bookings endpoint to the WooCommerce Account endpoints settings.
	 *
	 * @since 3.6.0
	 * @param array $settings Existing settings.
	 * @return array
	 */
	public function add_endpoint_account_settings( $settings ) {
		$new_setting = array(
			'title'    => __( 'Bookings', 'woocommerce-bookings' ),
			'desc'     => __( 'Endpoint for the "My account &rarr; Bookings" page.', 'woocommerce-bookings' ),
			'id'       => 'woocommerce_myaccount_bookings_endpoint',
			'type'     => 'text',
			'default'  => 'bookings',
			'desc_tip' => true,
		);

		// Insert before the end of the account endpoint section ('account_endpoint_options' sectionend).
		$index = array_search( 'account_endpoint_options', array_column( $settings, 'id' ), true );
		if ( false !== $index ) {
			// Find the sectionend entry (not the title).
			foreach ( $settings as $key => $setting ) {
				if ( isset( $setting['id'], $setting['type'] ) && 'account_endpoint_options' === $setting['id'] && 'sectionend' === $setting['type'] ) {
					array_splice( $settings, $key, 0, array( $new_setting ) );
					return $settings;
				}
			}
		}

		// Fallback: append to end.
		$settings[] = $new_setting;
		return $settings;
	}

	/**
	 * Register endpoint-specific hooks.
	 *
	 * Deferred to 'init' so that the 'woocommerce_bookings_account_endpoint'
	 * filter can be used from a theme's functions.php.
	 *
	 * @since 3.6.0
	 */
	public function register_endpoint_hooks() {
		add_filter( 'woocommerce_endpoint_' . $this->get_endpoint() . '_title', array( $this, 'change_endpoint_title' ), 10, 0 );
		add_action( 'woocommerce_account_' . $this->get_endpoint() . '_endpoint', array( $this, 'endpoint_content' ) );
	}

	/**
	 * Register new endpoint to use inside My Account page.
	 *
	 * @since 1.9.11
	 * @see https://developer.wordpress.org/reference/functions/add_rewrite_endpoint/
	 */
	public function add_endpoint() {
		add_rewrite_endpoint( $this->get_endpoint(), EP_PAGES );
	}

	/**
	 * Add new query var.
	 *
	 * @since 1.9.11
	 * @param array $vars
	 * @return string[]
	 */
	public function add_query_vars( $vars ) {
		$vars[] = $this->get_endpoint();
		return $vars;
	}

	/**
	 * Add bookings query var.
	 *
	 * @since 2.1.8
	 *
	 * @param array $vars Query vars.
	 *
	 * @return array altered query vars
	 */
	public function add_bookings_woocommerce_query_vars( $vars ) {
		$vars[ $this->get_endpoint() ] = $this->get_endpoint();

		return $vars;
	}

	/**
	 * Change title for bookings endpoint.
	 *
	 * @since 2.1.8
	 *
	 * @return string Endpoint title.
	 */
	public function change_endpoint_title() {
		/**
		 * Filters the endpoint title.
		 *
		 * @since 2.1.8
		 *
		 * @param string $title Endpoint title.
		 */
		return apply_filters( 'woocommerce_bookings_my_account_bookings_title', __( 'Bookings', 'woocommerce-bookings' ) );
	}

	/**
	 * Change endpoint title.
	 *
	 * @since 1.9.11
	 * @param string $title
	 * @return string
	 */
	public function endpoint_title( $title ) {
		global $wp_query;
		$is_endpoint = isset( $wp_query->query_vars[ $this->get_endpoint() ] );

		if ( $is_endpoint && ! is_admin() && is_main_query() && in_the_loop() && is_account_page() ) {
			$title = __( 'Bookings', 'woocommerce-bookings' );
			remove_filter( 'the_title', array( $this, 'endpoint_title' ) );
		}

		return $title;
	}

	/**
	 * Insert the new endpoint into the My Account menu.
	 *
	 * @since 1.9.11
	 * @param array $items
	 * @return array
	 */
	public function my_account_menu_item( $items ) {
		// Remove logout menu item.
		if ( array_key_exists( 'customer-logout', $items ) ) {
			$logout = $items['customer-logout'];
			unset( $items['customer-logout'] );
		}

		// Add bookings menu item.
		$items[ $this->get_endpoint() ] = __( 'Bookings', 'woocommerce-bookings' );

		// Add back the logout item.
		if ( isset( $logout ) ) {
			$items['customer-logout'] = $logout;
		}

		return $items;
	}

	/**
	 * Enqueue scripts and variables for my account pages Bookings section.
	 *
	 * @since 1.15.18
	 */
	public function my_account_scripts() {
		if ( ! is_account_page() ) {
			// Don't enqueue scripts if they are not needed.
			return;
		}

		$user_my_account_dependencies = wc_booking_get_script_dependencies( 'user-my-account', array( 'wc-bookings-date' ) );
		wp_enqueue_script( 'wc-bookings-user-my-account', WC_BOOKINGS_PLUGIN_URL . '/dist/user-my-account.js', $user_my_account_dependencies, WC_BOOKINGS_VERSION, true );

		$date_format = apply_filters( 'woocommerce_bookings_date_format', wc_bookings_date_format() );
		$time_format = apply_filters( 'woocommerce_bookings_time_format', ', ' . wc_bookings_time_format() );

		$my_account_params = array(
			'timezone_conversion' => 'yes' === WC_Bookings_Timezone_Settings::get( 'use_client_timezone' ),
			'datetime_format'     => wc_bookings_convert_to_moment_format( $date_format . $time_format ),
			'timezone_notice'     => __( 'All bookings start and end times except where stated differently are displayed in the following timezone: ', 'woocommerce-bookings' ),
			'cancel_confirmation' => __( 'Are you sure you want to cancel your booking?', 'woocommerce-bookings' ),
		);

		wp_localize_script( 'wc-bookings-user-my-account', 'wc_bookings_user_my_account_params', $my_account_params );
	}

	/**
	 * Endpoint HTML content.
	 *
	 * @param int $current_page
	 *
	 * @since    1.9.11
	 * @version  1.10.8
	 */
	public function endpoint_content( $current_page ) {
		$current_page = empty( $current_page ) ? 1 : absint( $current_page );
		$this->my_bookings( $current_page );
	}

	/**
	 * Show a users bookings in My Account > Bookings.
	 *
	 * @param int $current_page
	 *
	 * @since 1.8.0
	 * @since 1.13.0 Added Today's Bookings to My Account.
	 * @since 3.3.0 Updated titles.
	 */
	public function my_bookings( $current_page = 0 ) {
		$user_id      = get_current_user_id();
		$current_page = empty( $current_page ) ? 1 : absint( $current_page );

		$bookings_per_page = apply_filters( 'woocommerce_bookings_my_bookings_per_page', 10 );

		$today_bookings = WC_Booking_Data_Store::get_bookings_for_user( $user_id, apply_filters( 'woocommerce_bookings_my_bookings_today_query_args', array(
			'order_by'    => apply_filters( 'woocommerce_bookings_my_bookings_today_order_by', 'start_date' ),
			'order'       => 'ASC',
			'date_after'  => current_time( 'timestamp' ),
			'date_before' => strtotime( 'tomorrow', current_time( 'timestamp' ) ),
			'offset'      => ( $current_page - 1 ) * $bookings_per_page,
			'limit'       => $bookings_per_page + 1, // Increment to detect pagination.
		) ) );

		$past_bookings = WC_Booking_Data_Store::get_bookings_for_user( $user_id, apply_filters( 'woocommerce_bookings_my_bookings_past_query_args', array(
			'order_by'    => apply_filters( 'woocommerce_bookings_my_bookings_past_order_by', 'start_date' ),
			'order'       => 'DESC',
			'date_before' => current_time( 'timestamp' ),
			'offset'      => ( $current_page - 1 ) * $bookings_per_page,
			'limit'       => $bookings_per_page + 1, // Increment to detect pagination.
		) ) );

		$upcoming_bookings = WC_Booking_Data_Store::get_bookings_for_user( $user_id, apply_filters( 'woocommerce_bookings_my_bookings_upcoming_query_args', array(
			'order_by'   => apply_filters( 'woocommerce_bookings_my_bookings_upcoming_order_by', 'start_date' ),
			'order'      => 'ASC',
			'date_after' => strtotime( 'tomorrow', current_time( 'timestamp' ) ),
			'offset'     => ( $current_page - 1 ) * $bookings_per_page,
			'limit'      => $bookings_per_page + 1, // Increment to detect pagination.
		) ) );

		$tables = array();
		if ( ! empty( $today_bookings ) ) {
			$tables['today'] = array(
				'header'   => __( 'Today', 'woocommerce-bookings' ),
				'bookings' => $today_bookings,
			);
		}
		if ( ! empty( $upcoming_bookings ) ) {
			$tables['upcoming'] = array(
				'header'   => __( 'Upcoming', 'woocommerce-bookings' ),
				'bookings' => $upcoming_bookings,
			);
		}
		if ( ! empty( $past_bookings ) ) {
			$tables['past'] = array(
				'header'   => __( 'Past', 'woocommerce-bookings' ),
				'bookings' => $past_bookings,
			);
		}

		wc_get_template(
			'myaccount/bookings.php',
			apply_filters( 'woocommerce_bookings_my_bookings_template_args', array(
				'tables'            => apply_filters( 'woocommerce_bookings_account_tables', $tables ),
				'page'              => $current_page,
				'bookings_per_page' => $bookings_per_page,
				'endpoint'          => $this->get_endpoint(),
			) ),
			'woocommerce-bookings/', WC_BOOKINGS_TEMPLATE_PATH );
	}

	/**
	 * Called when an order is paid
	 * @param  int $order_id
	 */
	public function publish_bookings( $order_id ) {
		global $wpdb;

		$order          = wc_get_order( $order_id );
		$payment_method = is_callable( array( $order, 'get_payment_method' ) ) ? $order->get_payment_method() : $order->payment_method;

		$order_id = apply_filters( 'woocommerce_bookings_publish_bookings_order_id', $order_id );

		$bookings = WC_Booking_Data_Store::get_booking_ids_from_order_id( $order_id );

		// Don't publish bookings for COD orders, but still schedule their events
		$no_publish = $order->has_status( 'processing' ) && 'cod' === $payment_method;

		foreach ( $bookings as $booking_id ) {
			$booking = get_wc_booking( $booking_id );
			if ( ! $booking || $booking->has_status( 'was-in-cart' ) ) {
				continue;
			}

			if ( $no_publish ) {
				$booking->maybe_schedule_event( 'reminder' );
				$booking->maybe_schedule_event( 'complete' );

				// If the booking is in the cart, mark it as unpaid.
				if ( 'in-cart' === $booking->get_status() ) {
					$booking->set_status( 'unpaid' );
					$booking->save();
				}
			} else {
				$booking->paid();
			}
		}
	}

	/**
	 * Called when an order goes from Processing or Completed to Pending.
	 * Sets Bookings to status Unpaid.
	 *
	 * @since 1.15.14
	 * @param  int $order_id Id of current order.
	 * @return void
	 */
	public function mark_as_unpaid_bookings( $order_id ) {
		global $wpdb;
		$order    = wc_get_order( $order_id );
		$bookings = WC_Booking_Data_Store::get_booking_ids_from_order_id( $order_id );

		foreach ( $bookings as $booking_id ) {
			$booking = get_wc_booking( $booking_id );
			if ( ! $booking ) {
				continue;
			}
			$booking->set_status( 'unpaid' );
			$booking->save();
		}
	}

	/**
	 * Complete virtual booking orders,
	 *
	 * Hooked into a filter that changes the status for bookings.
	 *
	 * @param string        $order_status The order status.
	 * @param int           $order_id     The order ID. Zero (0) for unsaved orders.
	 * @param WC_Order|null $order        The order object. Optional. For backward compatibility this is optional
	 *                                    but it is recommended to be passed in to account for unsaved orders.
	 * @return string Modified order status.
	 */
	public function complete_order( $order_status, $order_id, $order = null ) {
		if ( null === $order ) {
			// Use legacy method to get order. Fails for unsaved orders.
			$order = wc_get_order( $order_id );
		}

		if ( ! ( $order instanceof WC_Order ) ) {
			// Return the status if the data type is unexpected.
			return $order_status;
		}

		if ( 'processing' === $order_status
			&& $order->has_status( array( 'on-hold', 'pending', 'failed' ) ) ) {
			$virtual_booking_order = false;

			if ( count( $order->get_items() ) < 1 ) {
				return $order_status;
			}

			foreach ( $order->get_items() as $item ) {
				if ( $item->is_type( 'line_item' ) ) {
					$product               = $item->get_product();
					$virtual_booking_order = $product && $product->is_virtual() && is_wc_booking_product( $product );
				}
				if ( ! $virtual_booking_order ) {
					break;
				}
			}
			// virtual order, mark as completed
			if ( $virtual_booking_order ) {
				return 'completed';
			}
		}

		// non-virtual order, return original status
		return $order_status;
	}

	/**
	 * Cancel bookings with order.
	 * @param  int $order_id
	 */
	public function cancel_bookings( $order_id ) {
		$order    = wc_get_order( $order_id );
		$bookings = WC_Booking_Data_Store::get_booking_ids_from_order_id( $order_id );

		$cancellable_statuses = get_wc_booking_statuses( 'cancel' );

		foreach ( $bookings as $booking_id ) {
			if ( self::is_syncing( $booking_id ) ) {
				continue;
			}
			$booking = get_wc_booking( $booking_id );
			if ( ! $booking ) {
				continue;
			}
			if ( ! in_array( $booking->get_status(), $cancellable_statuses, true ) ) {
				continue;
			}
			$booking->update_status( 'cancelled' );
		}

		self::syncing_stop( $order_id );
	}

	/**
	 * Cancel bookings when an order refunded partially.
	 *
	 * @since 1.10.0
	 *
	 * @version 1.10.0
	 *
	 * @see https://github.com/woocommerce/woocommerce-bookings/issues/817
	 *
	 * @param int $order_id Order ID.
	 */
	public function cancel_bookings_for_partial_refunds( $order_id ) {
		global $wpdb;

		$order              = wc_get_order( $order_id );
		$cancelled_bookings = array();

		// Prevents infinite loop during synch.
		$order->update_meta_data( '_booking_status_sync', true );
		$order->save_meta_data();

		// Collect booking IDs where refunded qty matches with its order item
		// being refunded.
		foreach ( $order->get_items() as $order_item_id => $item ) {
			$refunded_qty = $order->get_qty_refunded_for_item( $order_item_id );
			if ( 'line_item' === $item['type'] && 0 !== $refunded_qty ) {

				$booking_id = $wpdb->get_col( $wpdb->prepare( "SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_booking_order_item_id' AND meta_value = %d", $order_item_id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

				if ( $booking_id ) {
					$cancelled_bookings = array_merge(
						$cancelled_bookings,
						$booking_id
					);
				}
			}
		}

		// Update all cancelled bookings.
		$cancellable_statuses = get_wc_booking_statuses( 'cancel' );

		foreach ( $cancelled_bookings as $booking_id ) {
			// Skip booking that still in synch state.
			if ( get_post_meta( $booking_id, '_booking_status_sync', true ) ) {
				continue;
			}

			$booking = get_wc_booking( $booking_id );
			if ( ! $booking ) {
				continue;
			}
			if ( ! in_array( $booking->get_status(), $cancellable_statuses, true ) ) {
				continue;
			}
			$booking->update_status( 'cancelled' );
		}

		WC_Cache_Helper::get_transient_version( 'bookings', true );
		$order->delete_meta_data( '_booking_status_sync' );
		$order->save_meta_data();
	}

	/**
	 * Removes bookings related to the order being deleted.
	 *
	 * @param mixed $post_id ID of post being deleted
	 */
	public function delete_post( $post_id ) {
		if ( ! current_user_can( 'delete_posts' ) || ! $post_id ) {
			return;
		}

		if ( 'wc_booking' === get_post_type( $post_id ) ) {
			self::syncing_start( $post_id );

			$order_id = WC_Booking_Data_Store::get_booking_order_id( $post_id );
			$order = wc_get_order( $order_id );
			$item_count = is_a( $order, 'WC_Order' ) ? count( $order->get_items() ) : 0;

			if ( 1 === $item_count && $order_id && ! self::is_syncing( $order_id ) ) {
				$order->delete( true );
			}

			$this->clear_cron_hooks( (int) $post_id );

			self::syncing_stop( $post_id );
		}

		if ( WC_Booking_Order_Compat::is_shop_order( $post_id ) ) {
			self::syncing_start( $post_id );

			$bookings = WC_Booking_Data_Store::get_booking_ids_from_order_id( $post_id );

			foreach ( $bookings as $booking_id ) {
				if ( self::is_syncing( $booking_id ) ) {
					continue;
				}
				wp_delete_post( $booking_id, true );
			}

			self::syncing_stop( $post_id );
		}
	}

	/**
	 * Trash bookings with orders
	 *
	 * @param mixed $post_id
	 */
	public function trash_post( $post_id ) {
		if ( ! $post_id ) {
			return;
		}

		if ( 'wc_booking' === get_post_type( $post_id ) ) {
			self::syncing_start( $post_id );

			$order_id = WC_Booking_Data_Store::get_booking_order_id( $post_id );
			$order = wc_get_order( $order_id );
			$item_count = is_a( $order, 'WC_Order' ) ? count( $order->get_items() ) : 0;

			// only delete this order if this booking is the only item in it
			if ( 1 === $item_count && $order_id && ! self::is_syncing( $order_id ) ) {
				$order->delete();
			}

			self::syncing_stop( $post_id );

			// Clear booking slots transient.
			WC_Bookings_Cache::flush_all_booking_connected_transients( get_wc_booking( $post_id ) );
		}

		if ( WC_Booking_Order_Compat::is_shop_order( $post_id ) ) {
			self::syncing_start( $post_id );

			$bookings = WC_Booking_Data_Store::get_booking_ids_from_order_id( $post_id );

			foreach ( $bookings as $booking_id ) {
				if ( self::is_syncing( $booking_id ) ) {
					continue;
				}
				wp_trash_post( $booking_id );

				// Clear booking slots transient.
				WC_Bookings_Cache::flush_all_booking_connected_transients( get_wc_booking( $booking_id ) );
			}

			self::syncing_stop( $post_id );
		}
	}

	/**
	 * Untrash bookings with orders
	 *
	 * @param mixed $post_id
	 */
	public function untrash_post( $post_id ) {
		if ( ! $post_id ) {
			return;
		}

		if ( 'wc_booking' === get_post_type( $post_id ) ) {
			self::syncing_start( $post_id );

			$order_id = WC_Booking_Data_Store::get_booking_order_id( $post_id );

			if ( $order_id && ! self::is_syncing( $order_id ) ) {
				WC_Booking_Order_Compat::untrash_post( $order_id );
			}

			self::syncing_stop( $post_id );

			// Clear booking slots transient.
			WC_Bookings_Cache::flush_all_booking_connected_transients( get_wc_booking( $post_id ) );
		}

		if ( WC_Booking_Order_Compat::is_shop_order( $post_id ) ) {
			self::syncing_start( $post_id );

			$bookings = WC_Booking_Data_Store::get_booking_ids_from_order_id( $post_id );

			foreach ( $bookings as $booking_id ) {
				if ( self::is_syncing( $booking_id ) ) {
					continue;
				}
				wp_untrash_post( $booking_id );

				// Clear booking slots transient.
				WC_Bookings_Cache::flush_all_booking_connected_transients( get_wc_booking( $booking_id ) );
			}

			self::syncing_stop( $post_id );
		}
	}

	/**
	 * Keep the old status of bookings when untrashing a booking/order.
	 *
	 * @param string $new_status New status.
	 * @param int    $post_id Post ID.
	 * @param string $previous_status Previous status.
	 *
	 * @return string Updated status.
	 */
	public function untrash_post_status( $new_status, $post_id, $previous_status ) {
		if ( 'wc_booking' === get_post_type( $post_id ) ) {
			return $previous_status;
		}

		if ( WC_Booking_Order_Compat::is_shop_order( $post_id ) ) {
			$bookings = WC_Booking_Data_Store::get_booking_ids_from_order_id( $post_id );

			foreach ( $bookings as $booking_id ) {
				$booking = get_wc_booking( $booking_id );
				if ( ! $booking ) {
					continue;
				}
				$booking->update_status( $previous_status );
			}
		}

		return $new_status;
	}

	/**
	 * Clear cron hooks for booking
	 *
	 * @param mixed $post_id
	 */
	function clear_cron_hooks( $post_id ) {
		wp_clear_scheduled_hook( 'wc-booking-reminder', array( $post_id ) );
		wp_clear_scheduled_hook( 'wc-booking-complete', array( $post_id ) );
		wp_clear_scheduled_hook( 'wc-booking-remove-inactive-cart', array( $post_id ) );
	}


	/**
	 * Stops WC cancelling unpaid bookings orders.
	 *
	 * @param  bool $return
	 * @param  object $order
	 * @return bool
	 */
	public function prevent_cancel( $return, $order ) {
		$created_via    = is_callable( array( $order, 'get_created_via' ) ) ? $order->get_created_via() : $order->created_via;
		$payment_method = is_callable( array( $order, 'get_payment_method' ) ) ? $order->get_payment_method() : $order->payment_method;

		if ( 'bookings' === $created_via || 'wc-bookings-gateway' === $payment_method ) {
			return false;
		}
		return $return;
	}

	/**
	 * My Orders custom actions.
	 *
	 * Remove the pay button when the booking requires confirmation.
	 * @hooked woocommerce_my_account_my_orders_actions
	 * @param  array $actions
	 * @param  WC_Order $order
	 * @return array
	 */
	public function my_orders_actions( $actions, $order ) {
		if ( ! is_a( $order, 'WC_Order' ) ) {
			return $actions;
		}
		$payment_method = is_callable( array( $order, 'get_payment_method' ) ) ? $order->get_payment_method() : $order->payment_method;

		if ( $order->has_status( 'pending' ) && 'wc-bookings-gateway' === $payment_method ) {
			$status = array();

			foreach ( $order->get_items() as $order_item_id => $item ) {
				$booking_ids = WC_Booking_Data_Store::get_booking_ids_from_order_item_id( $order_item_id );
				if ( $booking_ids ) {
					foreach ( $booking_ids as $booking_id ) {
						try {
							$booking = new WC_Booking( $booking_id );
						} catch ( Exception $e ) {
							wc_get_logger()->error( $e->getMessage() );
							continue;
						}
						$status[] = $booking->get_status();
					}
				}
			}

			if ( in_array( 'pending-confirmation', $status ) && isset( $actions['pay'] ) ) {
				unset( $actions['pay'] );
			}
		}

		return $actions;
	}

	/**
	 * Sync booking customer ID when order customer ID is updated.
	 * Compatible with both CPT and HPOS (High-Performance Order Storage).
	 *
	 * @since 3.5.0
	 * @param WC_Order $order Order object.
	 */
	public function sync_booking_customer_id( $order ) {
		// Ensure we have a valid order object.
		if ( ! $order instanceof WC_Order ) {
			return;
		}

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

		// Check if customer_id has changed.
		$changes = $order->get_changes();
		if ( ! isset( $changes['customer_id'] ) ) {
			return;
		}

		$order_customer_id = $order->get_customer_id();
		$booking_ids       = WC_Booking_Data_Store::get_booking_ids_from_order_id( $order_id );

		// If no bookings found, nothing to sync.
		if ( empty( $booking_ids ) ) {
			return;
		}

		foreach ( $booking_ids as $booking_id ) {
			try {
				$booking = new WC_Booking( $booking_id );
			} catch ( Exception $e ) {
				wc_get_logger()->error( $e->getMessage() );
				continue;
			}

			// Only update if the customer ID has changed.
			if ( $booking->get_customer_id() !== $order_customer_id ) {
				$booking->set_customer_id( $order_customer_id );
				$booking->save( false );
			}
		}
	}

	/**
	 * Resolve WooCommerce Analytics customer ID before a booking is saved.
	 *
	 * Handles all scenarios: new bookings, customer_id changes (direct or
	 * via order sync), and deferred resolution when woo_customer_id is still 0.
	 *
	 * @since 3.5.0
	 * @param WC_Booking $booking Booking object.
	 */
	public function sync_woo_customer_id_on_booking_save( $booking ) {
		$changes = $booking->get_changes();

		// If woo_customer_id was explicitly set, resolve user_id from it
		// — but only when customer_id wasn't also explicitly provided.
		if ( isset( $changes['woo_customer_id'] ) ) {
			if ( ! isset( $changes['customer_id'] ) ) {
				$woo_customer_id = $booking->get_woo_customer_id();

				if ( $woo_customer_id ) {
					$booking->set_customer_id( self::get_user_id_from_woo_customer_id( $woo_customer_id ) );
				} else {
					$booking->set_customer_id( 0 );
				}
			}

			return;
		}

		$customer_id_changed = isset( $changes['customer_id'] );
		$current_woo_id      = $booking->get_woo_customer_id();

		// Already resolved and customer didn't change — nothing to do.
		if ( $current_woo_id && ! $customer_id_changed ) {
			return;
		}

		if ( $customer_id_changed ) {
			// Resolve from the new user_id directly. We cannot use the
			// order-based resolution because the order may not be saved yet.
			$new_woo_id  = 0;
			$customer_id = $booking->get_customer_id();

			if ( $customer_id ) {
				$customers_ds = 'Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore';
				if ( class_exists( $customers_ds ) ) {
					$wc_customer_id = $customers_ds::get_customer_id_by_user_id( $customer_id );
					if ( $wc_customer_id ) {
						$new_woo_id = absint( $wc_customer_id );
					}
				}
			}

			if ( $new_woo_id !== $current_woo_id ) {
				$booking->set_woo_customer_id( $new_woo_id );
			}
			return;
		}

		// woo_customer_id is 0 and customer_id unchanged — try a read-only lookup.
		// If the analytics customer row does not exist yet (typical for guest
		// checkouts before WC's own analytics sync has run), the value stays 0
		// and is backfilled later by backfill_woo_customer_id_for_order().
		$woo_customer_id = $this->lookup_woo_customer_id( $booking );
		if ( $woo_customer_id ) {
			$booking->set_woo_customer_id( $woo_customer_id );
		}
	}

	/**
	 * Resolve the WordPress user ID for a WooCommerce Analytics customer ID.
	 *
	 * There is no public WooCommerce core helper for this reverse lookup, so
	 * Bookings keeps the query in one place and reuses it across save/create flows.
	 *
	 * @param int $woo_customer_id WooCommerce Analytics customer ID.
	 * @return int
	 */
	public static function get_user_id_from_woo_customer_id( int $woo_customer_id ): int {
		if ( ! $woo_customer_id ) {
			return 0;
		}

		global $wpdb;

		$table_name = $wpdb->prefix . 'wc_customer_lookup';

		// phpcs:disable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$user_id = $wpdb->get_var(
			$wpdb->prepare(
				'SELECT user_id FROM ' . $table_name . ' WHERE customer_id = %d LIMIT 1',
				$woo_customer_id
			)
		);
		// phpcs:enable WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching

		return absint( $user_id );
	}

	/**
	 * Attaches a newly created user (during checkout) to a booking.
	 *
	 * @since 3.5.0
	 */
	public function attach_new_user( $booking_id, $booking ) {
		if ( 0 === $booking->get_customer_id() && get_current_user_id() > 0 ) {
			$booking->set_customer_id( get_current_user_id() );
			$booking->save( false );
		}
	}

	/**
	 * Look up an existing WooCommerce Analytics customer ID for a booking.
	 *
	 * Read-only by design: never writes to the `wc_customer_lookup` table.
	 * Writes are owned by WooCommerce's own Analytics sync (queued via
	 * Action Scheduler on order create/update). When the row does not exist
	 * yet, the caller leaves woo_customer_id at 0 and the value is backfilled
	 * by backfill_woo_customer_id_for_order() once WC fires
	 * `woocommerce_analytics_update_order_stats`.
	 *
	 * @since 3.5.2
	 * @param WC_Booking $booking Booking object.
	 * @return int|false WC Analytics customer ID or false if not yet available.
	 */
	private function lookup_woo_customer_id( $booking ) {
		$customers_ds = 'Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore';

		if ( ! class_exists( $customers_ds ) ) {
			return false;
		}

		$order_id = $booking->get_order_id();
		if ( $order_id ) {
			$order = wc_get_order( $order_id );
			if ( $order ) {
				$wc_customer_id = $this->lookup_woo_customer_id_for_order( $order );
				if ( $wc_customer_id ) {
					return $wc_customer_id;
				}
			}
		}

		$user_id = $booking->get_customer_id();
		if ( $user_id ) {
			$wc_customer_id = $customers_ds::get_customer_id_by_user_id( $user_id );
			if ( $wc_customer_id ) {
				return absint( $wc_customer_id );
			}
		}

		return false;
	}

	/**
	 * Read-only lookup of the analytics customer ID for an order.
	 *
	 * Prefers the registered-user lookup; falls back to the guest-by-email
	 * lookup when the order has no user_id but does have a billing email.
	 *
	 * @since 3.5.2
	 * @param WC_Abstract_Order $order Order object.
	 * @return int 0 when no row exists yet.
	 */
	private function lookup_woo_customer_id_for_order( WC_Abstract_Order $order ) {
		$customers_ds = 'Automattic\WooCommerce\Admin\API\Reports\Customers\DataStore';

		if ( ! class_exists( $customers_ds ) ) {
			return 0;
		}

		$user_id = $order->get_user_id();
		if ( $user_id ) {
			$wc_customer_id = $customers_ds::get_customer_id_by_user_id( $user_id );
			if ( $wc_customer_id ) {
				return absint( $wc_customer_id );
			}
		}

		$billing_email = $order->get_billing_email( 'edit' );
		if ( $billing_email && method_exists( $customers_ds, 'get_guest_id_by_email' ) ) {
			$wc_customer_id = $customers_ds::get_guest_id_by_email( $billing_email );
			if ( $wc_customer_id ) {
				return absint( $wc_customer_id );
			}
		}

		return 0;
	}

	/**
	 * Backfill woo_customer_id on bookings after WooCommerce Analytics syncs the order.
	 *
	 * `woocommerce_analytics_update_order_stats` fires after
	 * OrdersStatsDataStore::update() has populated the `wc_customer_lookup` row
	 * for the order. At that point the read-only lookup performed on booking
	 * save can succeed, so we revisit any bookings on this order whose
	 * woo_customer_id is still unresolved.
	 *
	 * @since 3.5.2
	 * @param int $order_id Order ID.
	 */
	public function backfill_woo_customer_id_for_order( $order_id ) {
		$order_id = absint( $order_id );
		if ( ! $order_id ) {
			return;
		}

		$booking_ids = WC_Booking_Data_Store::get_booking_ids_from_order_id( $order_id );
		if ( empty( $booking_ids ) ) {
			return;
		}

		$order = wc_get_order( $order_id );
		if ( ! $order ) {
			return;
		}

		$wc_customer_id = $this->lookup_woo_customer_id_for_order( $order );
		if ( ! $wc_customer_id ) {
			return;
		}

		foreach ( $booking_ids as $booking_id ) {
			try {
				$booking = new WC_Booking( $booking_id );
			} catch ( Exception $e ) {
				wc_get_logger()->error( $e->getMessage() );
				continue;
			}

			if ( absint( $booking->get_woo_customer_id() ) === $wc_customer_id ) {
				continue;
			}

			$booking->set_woo_customer_id( $wc_customer_id );
			$booking->save( false );
		}
	}

	/**
	 * Cancel order with bookings.
	 *
	 * @param  int $booking_id
	 */
	public function maybe_cancel_order( $booking_id ) {
		$this->maybe_update_order( $booking_id, 'cancelled' );
	}

	/**
	 * Track the cancel timestamp for a booking.
	 *
	 * @since 3.0.0
	 *
	 * @param  int $booking_id Booking ID.
	 */
	public function track_cancel_timestamp( $booking_id ) {
		$booking = get_wc_booking( $booking_id );
		if ( ! $booking ) {
			return;
		}
		$booking->set_date_cancelled( time() );
		$booking->save( false );
	}

	/**
	 * Mark the booking as unattended when it is cancelled.
	 *
	 * @param int $booking_id Booking ID.
	 */
	public function mark_as_unattended( $booking_id ) {
		$booking = get_wc_booking( $booking_id );
		if ( ! $booking ) {
			return;
		}
		$booking->set_attendance_status( 'unattended' );
		$booking->save( false );
	}

	/**
	 * Sync order with bookings (paid) only in case where the booking is the only item in the order.
	 *
	 * @param  int $booking_id
	 */
	public function maybe_process_order( $booking_id ) {
		$this->maybe_update_order( $booking_id, 'processing' );
	}

	/**
	 * Sync order with bookings (unpaid) only in case where the booking is the only item in the order.
	 *
	 * @param  int $booking_id
	 */
	public function maybe_pending_order( $booking_id ) {
		$this->maybe_update_order( $booking_id, 'pending' );
	}

	/**
	 * Maybe set attendance status to attended when booking becomes complete.
	 *
	 * @param int        $booking_id Booking ID.
	 * @param WC_Booking $booking    Booking object.
	 */
	public function maybe_set_as_attended( $booking_id, $booking ) {
		if ( ! is_a( $booking, 'WC_Booking' ) ) {
			return;
		}

		if ( wc_bookings_maybe_mark_completed_bookings_as_attended( $booking ) ) {
			$booking->set_attendance_status( 'attended' );
			$booking->save( false );
		}
	}

	/**
	 * Update booking's related order
	 * This only applies if the order has only the booking as an item.
	 *
	 * @param  int $booking_id
	 * @param  string $status
	 */
	private function maybe_update_order( $booking_id, $status ) {
		// Prevents infinite loop during synchronization
		self::syncing_start( $booking_id );

		$order_id  = WC_Booking_Data_Store::get_booking_order_id( $booking_id );
		$order     = wc_get_order( $order_id );

		if ( ! is_a( $order, 'WC_Order' ) ) {
			self::syncing_stop( $booking_id );
			return;
		}

		$completed = 'processing' === $status && 'completed' === $order->get_status();
		$refunded  = 'cancelled' === $status && 'refunded' === $order->get_status();

		// Do not update status of completed or refunded orders.
		if ( self::is_syncing( $order_id ) || $completed || $refunded ) {
			self::syncing_stop( $booking_id );
			return;
		}

		// Do not change a failed order to "pending" when the booking goes to "unpaid".
		if ( 'pending' === $status && 'failed' === $order->get_status() && 'unpaid' === get_wc_booking( $booking_id )->get_status() ) {
			self::syncing_stop( $booking_id );

			return;
		}

		// Only update status if the order has 1 booking
		if ( 1 === count( $order->get_items() ) ) {
			$order->update_status( $status );
		} elseif ( 'cancelled' === $status ) {
			$booking_ids = WC_Booking_Data_Store::get_order_contains_only_bookings( $order );

			if ( empty( $booking_ids ) ) {
				self::syncing_stop( $booking_id );
				return;
			}

			$booking_statuses = array_map( function( $booking_id ) {
				return get_wc_booking( $booking_id )->get_status();
			}, $booking_ids );

			// Cancel the order only if all Bookings are cancelled
			if ( array( $status ) === array_unique( $booking_statuses ) ) {
				$order->update_status( $status );
			}
		}

		self::syncing_stop( $booking_id );
	}

	/**
	 * Scheduling an event for a failed order if it has bookings.
	 *
	 * @since 1.11.0
	 *
	 * @param integer $order_id The order that failed.
	 */
	public function schedule_failed_order_event( $order_id ) {
		$booking_ids = WC_Booking_Data_Store::get_booking_ids_from_order_id( $order_id );
		if ( empty( $booking_ids ) ) {
			return;
		}

		// Revert the booking status to in-cart to enable the re-payment with the same order and booking.
		foreach ( $booking_ids as $booking_id ) {
			$booking = get_wc_booking( $booking_id );
			if ( ! $booking ) {
				continue;
			}
			$booking->set_status( 'in-cart' );
			$booking->save();
		}

		$args = array(
			'order_id'    => $order_id,
			'booking_ids' => $booking_ids,
		);

		$timestamp = apply_filters( 'woocommerce_bookings_failed_order_expire_scheduled_time_stamp', time() + ( HOUR_IN_SECONDS ) );
		as_schedule_single_action( $timestamp, 'woocommerce_bookings_failed_order_expired', $args );
	}

	/**
	 * Responding to scheduled event for a failed booking.
	 *
	 * @since 1.11.0
	 *
	 * @param integer $order_id The order that failed.
	 * @param array   $booking_ids Attached to failed order.
	 */
	public function handle_failed_order_scheduled_event( $order_id = 0, $booking_ids = array() ) {
		$order = wc_get_order( $order_id );
		if ( ! is_a( $order, 'WC_Order' )
			|| 'failed' !== $order->get_status()
			|| empty( $booking_ids ) ) {
			return;
		}

		foreach ( $booking_ids as $booking_id ) {
			$booking = get_wc_booking( $booking_id );
			if ( $booking && $booking->get_order_id() === $order_id ) {
				$booking->set_status( 'cancelled' );
				$booking->save();
			}
		}
	}

	/**
	 * Add edit order link for admin.
	 * Add view my bookings link for customers.
	 * Works only if order has booking(s).
	 *
	 * @param WC_Order $order WC_Order object.
	 * @param bool     $sent_to_admin Whether to send email notification to admin or not.
	 *
	 * @since 1.15.57
	 * @return void
	 */
	public function add_admin_calender_link( $order, $sent_to_admin ) {
		$booking_ids = WC_Booking_Data_Store::get_order_contains_only_bookings( $order );

		// Proceed only if order has booking(s).
		if ( ! empty( $booking_ids ) ) {
			// For admin, display order edit page URL.
			if ( $sent_to_admin && $order->get_order_item_totals() ) {
				?>
				<div class="wc-booking-summary-actions" style="margin-bottom: 30px;">
					<a href="<?php echo esc_url( $order->get_edit_order_url() ); ?>"><?php esc_html_e( 'View order &rarr;', 'woocommerce-bookings' ); ?></a>
					<br/>
					<a href="<?php echo esc_url( wc_bookings_get_calendar_url() ); ?>"><?php esc_html_e( 'View Booking Calendar &rarr;', 'woocommerce-bookings' ); ?></a>
				</div>
				<?php
			} elseif ( function_exists( 'wc_get_endpoint_url' ) && wc_get_page_id( 'myaccount' ) && $order->get_customer_id() ) {
				// For customers, display 'my bookings' page URL.
				?>
				<div class="wc-booking-summary-actions" style="margin-bottom: 30px;">
					<a href="<?php echo esc_url( wc_get_endpoint_url( $this->get_endpoint(), '', wc_get_page_permalink( 'myaccount' ) ) ); ?>"><?php esc_html_e( 'View my bookings &rarr;', 'woocommerce-bookings' ); ?></a>
				</div>
				<?php
			}
		}
	}

	/**
	 * Format booking ID meta.
	 *
	 * The booking ID meta is stored as an array, but meta data with scalar values are ignored in `WC_Order_Item::get_formatted_meta_data`.
	 * This also prevents them from showing in the REST API.
	 * This method ensures it's only exposed in REST API responses and remains hidden in all user-facing pages.
	 *
	 * @param array         $formatted_meta_data Formatted meta data.
	 * @param WC_Order_Item $item Order item.
	 * @return array
	 */
	public function order_item_get_formatted_meta_data( $formatted_meta_data, $item ) {
		// Don't expose _booking_id in emails.
		if ( doing_action( 'woocommerce_email_order_details' ) || doing_action( 'woocommerce_email_fulfillment_details' ) ) {
			return $formatted_meta_data;
		}

		// Reveal the meta only in REST API context.
		if ( ! WC_Bookings_Core_Compatibility::is_rest_api_request() ) {
			return $formatted_meta_data;
		}

		$booking_id_data = $item->get_meta( '_booking_id' );

		if ( is_array( $booking_id_data ) && ! empty( $booking_id_data ) ) {
			$formatted_meta_data[] = (object) array(
				'key'           => '_booking_id',
				'value'         => implode( ', ', $booking_id_data ),
				'display_key'   => '_booking_id',
				'display_value' => implode( ', ', $booking_id_data ),
			);
		}
		return $formatted_meta_data;
	}
}
