<?php
/**
 * WooCommerce Bookings Functions
 *
 * @package WooCommerce Bookings
 * @version 3.7.0
 */

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

/**
 * Get a booking object
 * @param  int $id
 * @return WC_Booking|false
 */
function get_wc_booking( $id ) {
	try {
		return new WC_Booking( $id );
	} catch ( Exception $e ) {
		return false;
	}
}

/**
 * Get a bookable product object
 * @param  int $id
 * @return WC_Product_Booking|false
 */
function get_wc_product_booking( $id ) {
	try {
		return new WC_Product_Booking( $id );
	} catch ( Exception $e ) {
		return false;
	}
}

/**
 * Get the edit URL for a booking in the admin.
 *
 * @since 3.4.0
 *
 * @param int|WC_Booking $booking Booking ID or object.
 * @return string
 */
function wc_bookings_get_edit_booking_url( $booking ) {
	$booking_id = $booking instanceof WC_Booking ? $booking->get_id() : $booking;
	$booking_id = absint( $booking_id );

	if ( ! $booking_id ) {
		return '';
	}

	$url = admin_url( 'post.php?post=' . $booking_id . '&action=edit' );

	/**
	 * Filter the edit booking admin URL.
	 *
	 * @since 3.4.0
	 *
	 * @param string $url        The edit booking URL.
	 * @param int    $booking_id The booking ID.
	 * @param mixed  $booking    The booking object or ID.
	 */
	return apply_filters( 'woocommerce_bookings_edit_booking_url', $url, $booking_id, $booking );
}

/**
 * Get the booking calendar URL in the admin.
 *
 * @since 3.4.0
 *
 * @return string
 */
function wc_bookings_get_calendar_url() {
	$url = admin_url( 'edit.php?post_type=wc_booking&page=booking_calendar&view=month' );

	/**
	 * Filter the booking calendar URL.
	 *
	 * @since 3.4.0
	 *
	 * @param string $url The booking calendar URL.
	 */
	return apply_filters( 'woocommerce_bookings_calendar_url', $url );
}

/**
 * Get a booking resource.
 *
 * @since 3.0.0
 *
 * @param  int $id Resource ID.
 * @param  int $product_id Product ID (optional).
 * @return mixed|WC_Product_Booking_Resource|false
 */
function get_wc_product_booking_resource( $id, $product_id = 0 ) {
	$post_type = get_post_type( $id );
	if ( ! in_array( $post_type, wc_booking_get_product_resource_post_types(), true ) ) {
		return false;
	}

	$classname = _wc_booking_get_product_resource_classname_from_post_type( $post_type );
	try {
		$resource = new $classname( $id, $product_id );
		if ( ! $resource->get_id() ) {
			return false;
		}
		return $resource;
	} catch ( Exception $e ) {
		return false;
	}
}

/**
 * Get product booking resource post types.
 *
 * @since 3.0.0
 *
 * @return array
 */
function wc_booking_get_product_resource_post_types() {
	$types = array( 'bookable_resource' );

	if ( defined( 'WC_BOOKINGS_EXPERIMENTAL_ENABLED' ) && WC_BOOKINGS_EXPERIMENTAL_ENABLED ) {
		$types[] = 'bookable_team_member';
	}

	return $types;
}

/**
 * Get the product booking class name from the post type.
 *
 * @since 3.0.0
 *
 * @param string $post_type The post type.
 * @return string
 */
function _wc_booking_get_product_resource_classname_from_post_type( $post_type ) {
	if ( ! in_array( $post_type, wc_booking_get_product_resource_post_types(), true ) ) {
		return 'WC_Product_Booking_Resource';
	}

	if ( 'bookable_team_member' === $post_type ) {
		return 'WC_Product_Booking_Team_Member';
	}

	return 'WC_Product_Booking_Resource';
}

/**
 * Gets a cost based on the base cost and default resource.
 *
 * @param  WC_Product_Booking $product
 * @return string
 */
function wc_booking_calculated_base_cost( $product ) {
	wc_deprecated_function( __FUNCTION__, '1.15.0', 'WC_Bookings_Cost_Calculation::calculated_base_cost()' );

	return WC_Bookings_Cost_Calculation::calculated_base_cost( $product );
}

/**
 * Santiize and format a string into a valid 24 hour time
 * @return string
 */
function wc_booking_sanitize_time( $raw_time ) {
	$time = wc_clean( $raw_time );
	$time = date( 'H:i', strtotime( $time ) );
	return $time;
}

/**
 * Returns true if the product is a booking product, false if not
 *
 * @param WC_Product $product The product to check.
 * @return bool
 */
function is_wc_booking_product( $product ) {
	if ( ! is_a( $product, 'WC_Product' ) ) {
		return false;
	}

	$booking_product_types = wc_bookings_get_product_types();
	return isset( $product ) && $product->is_type( $booking_product_types );
}

/**
 * Get the booking product types.
 *
 * @since 3.0.0
 *
 * @return array
 */
function wc_bookings_get_product_types() {

	/**
	 * Filter the booking product types.
	 *
	 * @since 1.0.0
	 *
	 * @param array $product_types The booking product types.
	 * @return array The booking product types.
	 */
	return apply_filters( 'woocommerce_bookings_product_types', array( 'booking' ) );
}

/**
 * Convert key to a nice readable label
 * @param  string $key
 * @return string
 */
function get_wc_booking_data_label( $key, $product ) {
	$labels = apply_filters( 'woocommerce_bookings_data_labels', array(
		'type'     => ( $product->get_resource_label() ? $product->get_resource_label() : __( 'Booking Type', 'woocommerce-bookings' ) ),
		'date'     => __( 'Booking Date', 'woocommerce-bookings' ),
		'time'     => __( 'Booking Time', 'woocommerce-bookings' ),
		'timezone' => __( 'Time Zone', 'woocommerce-bookings' ),
		'duration' => __( 'Duration', 'woocommerce-bookings' ),
		'persons'  => __( 'Person(s)', 'woocommerce-bookings' ),
	) );

	if ( ! array_key_exists( $key, $labels ) ) {
		return $key;
	}

	return $labels[ $key ];
}

/**
 * Convert status to human readable label.
 *
 * @since  1.10.0
 * @param  string $status
 * @return string
 */
function wc_bookings_get_status_label( $status ) {
	$statuses = array(
		'unpaid'               => __( 'Unpaid', 'woocommerce-bookings' ),
		'pending-confirmation' => __( 'Pending Confirmation', 'woocommerce-bookings' ),
		'confirmed'            => __( 'Confirmed', 'woocommerce-bookings' ),
		'paid'                 => __( 'Paid', 'woocommerce-bookings' ),
		'cancelled'            => __( 'Cancelled', 'woocommerce-bookings' ),
		'complete'             => __( 'Complete', 'woocommerce-bookings' ),
		'in-cart'              => __( 'In Cart', 'woocommerce-bookings' ),
	);

	/**
	 * Filter the return value of wc_bookings_get_status_label.
	 *
	 * @since 1.11.0
	 */
	$statuses = apply_filters( 'woocommerce_bookings_get_status_label', $statuses );

	return array_key_exists( $status, $statuses ) ? $statuses[ $status ] : $status;
}

/**
 * Returns a list of booking statuses.
 *
 * @since 1.9.13 Add new parameter that allows globalised status strings as part of the array.
 * @param  string $context An optional context (filters) for user or cancel statuses
 * @param boolean $include_translation_strings. Defaults to false. This introduces status translations text string. In future (2.0) should default to true.
 * @return array $statuses
 */
function get_wc_booking_statuses( $context = 'fully_booked', $include_translation_strings = false ) {
	if ( 'user' === $context ) {
		$statuses = apply_filters( 'woocommerce_bookings_for_user_statuses', array(
			'unpaid'               => __( 'Unpaid', 'woocommerce-bookings' ),
			'pending-confirmation' => __( 'Pending Confirmation', 'woocommerce-bookings' ),
			'confirmed'            => __( 'Confirmed', 'woocommerce-bookings' ),
			'paid'                 => __( 'Paid', 'woocommerce-bookings' ),
			'cancelled'            => __( 'Cancelled', 'woocommerce-bookings' ),
			'complete'             => __( 'Complete', 'woocommerce-bookings' ),
		) );
	} elseif ( 'cancel' === $context ) {
		$statuses = apply_filters( 'woocommerce_valid_booking_statuses_for_cancel', array(
			'unpaid'               => __( 'Unpaid', 'woocommerce-bookings' ),
			'pending-confirmation' => __( 'Pending Confirmation', 'woocommerce-bookings' ),
			'confirmed'            => __( 'Confirmed', 'woocommerce-bookings' ),
			'paid'                 => __( 'Paid', 'woocommerce-bookings' ),
		) );
	} elseif ( 'scheduled' === $context ) {
		$statuses = apply_filters( 'woocommerce_bookings_scheduled_statuses', array(
			'confirmed'            => __( 'Confirmed', 'woocommerce-bookings' ),
			'paid'                 => __( 'Paid', 'woocommerce-bookings' ),
		) );
	} else {
		$statuses = apply_filters( 'woocommerce_bookings_fully_booked_statuses', array(
			'unpaid'               => __( 'Unpaid', 'woocommerce-bookings' ),
			'pending-confirmation' => __( 'Pending Confirmation', 'woocommerce-bookings' ),
			'confirmed'            => __( 'Confirmed', 'woocommerce-bookings' ),
			'paid'                 => __( 'Paid', 'woocommerce-bookings' ),
			'complete'             => __( 'Complete', 'woocommerce-bookings' ),
			'in-cart'              => __( 'In Cart', 'woocommerce-bookings' ),
		) );
	}

	/**
	 * Filter the return value of get_wc_booking_statuses.
	 *
	 * @since 1.11.0
	 */
	$statuses = apply_filters( 'woocommerce_bookings_get_wc_booking_statuses', $statuses );

	// backwards compatibility
	return $include_translation_strings ? $statuses : array_keys( $statuses );
}

/**
 * Validate and create a new booking manually.
 *
 * @version  1.10.7
 * @see      WC_Booking::new_booking() for available $new_booking_data args
 * @param    int    $product_id you are booking
 * @param    array  $new_booking_data
 * @param    string $status
 * @param    bool   $exact If false, the function will look for the next available block after your start date if the date is unavailable.
 * @return   mixed  WC_Booking object on success or false on fail
 */
function create_wc_booking( $product_id, $new_booking_data = array(), $status = 'confirmed', $exact = false ) {
	// Merge booking data
	$defaults = array(
		'product_id'  => $product_id, // Booking ID
		'start_date'  => '',
		'end_date'    => '',
		'resource_id' => '',
	);

	$new_booking_data = wp_parse_args( $new_booking_data, $defaults );
	$product          = wc_get_product( $product_id );
	$start_date       = $new_booking_data['start_date'];
	$end_date         = $new_booking_data['end_date'];
	$max_date         = $product->get_max_date();
	$all_day          = isset( $new_booking_data['all_day'] ) && $new_booking_data['all_day'] ? true : false;
	$qty = 1;

	if ( $product->has_person_qty_multiplier() && ! empty( $new_booking_data['persons'] ) ) {
		if ( is_array( $new_booking_data['persons'] ) ) {
			$qty = array_sum( $new_booking_data['persons'] );
		} else {
			$qty = $new_booking_data['persons'];
			$new_booking_data['persons'] = array( $qty );
		}
	}

	// If not set, use next available
	if ( ! $start_date ) {
		$min_date   = $product->get_min_date();
		$start_date = strtotime( "+{$min_date['value']} {$min_date['unit']}", current_time( 'timestamp' ) );
	}

	// If not set, use next available + block duration
	if ( ! $end_date ) {
		$end_date = strtotime( '+' . $product->get_duration() . ' ' . $product->get_duration_unit(), $start_date );
	}

	$searching = true;
	$date_diff = $all_day ? DAY_IN_SECONDS : $end_date - $start_date;

	while ( $searching ) {

		$available_bookings = $product->get_available_bookings( $start_date, $end_date, $new_booking_data['resource_id'], $qty );

		if ( $available_bookings && ! is_wp_error( $available_bookings ) ) {

			if ( ! $new_booking_data['resource_id'] && is_array( $available_bookings ) ) {
				$new_booking_data['resource_id'] = current( array_keys( $available_bookings ) );
			}

			$searching = false;

		} else {
			if ( $exact ) {
				return false;
			}

			$start_date += $date_diff;
			$end_date   += $date_diff;

			if ( $end_date > strtotime( "+{$max_date['value']} {$max_date['unit']}" ) ) {
				return false;
			}
		}
	}

	// Set dates
	$new_booking_data['start_date'] = $start_date;
	$new_booking_data['end_date']   = $end_date;

	// Create it
	$new_booking = get_wc_booking( $new_booking_data );
	if ( $new_booking ) {
		$new_booking->create( $status );
	}

	return $new_booking;
}

/**
 * Check if product/booking requires confirmation.
 *
 * @param  int $id Product ID.
 *
 * @return bool
 */
function wc_booking_requires_confirmation( $id ) {
	$product = wc_get_product( $id );

	if (
		is_object( $product )
		&& is_wc_booking_product( $product )
		&& $product->requires_confirmation()
	) {
		return true;
	}

	return false;
}

/**
 * Check if the cart has booking that requires confirmation.
 *
 * @return bool
 */
function wc_booking_cart_requires_confirmation() {
	$requires = false;

	if ( ! empty( WC()->cart->cart_contents ) ) {
		foreach ( WC()->cart->cart_contents as $item ) {
			if ( wc_booking_requires_confirmation( $item['product_id'] ) ) {
				$requires = true;
				break;
			}
		}
	}

	return $requires;
}

/**
 * Check if the order has booking that requires confirmation.
 *
 * @param  WC_Order $order
 *
 * @return bool
 */
function wc_booking_order_requires_confirmation( $order ) {
	$requires = false;

	if ( $order ) {
		foreach ( $order->get_items() as $item ) {
			if ( wc_booking_requires_confirmation( $item['product_id'] ) ) {
				$requires = true;
				break;
			}
		}
	}

	return $requires;
}

/**
 * Check if user has location based timezone selected in settings.
 *
 * @since 1.13.0
 *
 * @return bool
 */
function wc_booking_has_location_timezone_set() {

	$timezone = get_option( 'timezone_string' );

	if ( ! empty( $timezone ) && false !== strpos( $timezone, 'Etc/GMT' ) ) {
		$timezone = '';
	}

	if ( empty( $timezone ) ) {
		return false;
	}

	return true;
}

/**
 * Clear the Bookings timezone string cache.
 *
 * @return void
 */
function wc_booking_clear_timezone_string_cache() {
	wp_cache_delete( 'wc_bookings_timezone_string' );
}

/**
 * Get timezone string.
 *
 * inspired by https://wordpress.org/plugins/event-organiser/
 *
 * @return string
 */
function wc_booking_get_timezone_string() {
	$timezone = wp_cache_get( 'wc_bookings_timezone_string' );

	if ( false === $timezone ) {
		$timezone   = get_option( 'timezone_string' );
		$gmt_offset = get_option( 'gmt_offset' );

		// Remove old Etc mappings. Fallback to gmt_offset.
		if ( ! empty( $timezone ) && false !== strpos( $timezone, 'Etc/GMT' ) ) {
			$timezone = '';
		}

		if ( empty( $timezone ) && 0 != $gmt_offset ) {
			// Use gmt_offset
			$gmt_offset   *= 3600; // convert hour offset to seconds
			$allowed_zones = timezone_abbreviations_list();

			foreach ( $allowed_zones as $abbr ) {
				foreach ( $abbr as $city ) {
					if ( $city['offset'] == $gmt_offset ) {
						$timezone = $city['timezone_id'];
						break 2;
					}
				}
			}
		}

		// Issue with the timezone selected, set to 'UTC'
		if ( empty( $timezone ) ) {
			$timezone = 'UTC';
		}

		// Cache the timezone string.
		wp_cache_set( 'wc_bookings_timezone_string', $timezone );
	}

	return $timezone;
}

/**
 * Format a fake-UTC booking timestamp as ISO 8601 with the current site offset.
 *
 * Bookings store start/end as integers whose UTC representation equals the site-local
 * wall clock (see WC_Booking_Data_Store). This helper turns one of those timestamps
 * into a portable, unambiguous string of the form `2026-05-02T09:00:00-04:00`.
 *
 * @since 3.6.0
 *
 * @param int|string $fake_utc_ts Fake-UTC unix timestamp. Empty string or 0 returns ''.
 * @return string ISO 8601 datetime with offset, or empty string.
 */
function wc_bookings_format_csv_datetime( $fake_utc_ts ) {
	if ( empty( $fake_utc_ts ) ) {
		return '';
	}

	$site_tz = new DateTimeZone( wc_booking_get_timezone_string() );

	// gmdate gives the wall clock encoded in the fake-UTC timestamp,
	// which IS the site-local wall clock by storage convention.
	$wall_clock = gmdate( 'Y-m-d H:i:s', (int) $fake_utc_ts );

	try {
		$dt = new DateTimeImmutable( $wall_clock, $site_tz );
	} catch ( Exception $e ) {
		return '';
	}

	// `c` produces ISO 8601 with offset, e.g. 2026-05-02T09:00:00-04:00.
	return $dt->format( 'c' );
}

/**
 * Parse a CSV datetime value into a fake-UTC booking timestamp.
 *
 * Accepts:
 * - ISO 8601 with offset, e.g. `2026-05-02T09:00:00-04:00` (preferred). If the offset
 *   matches the destination site's offset for that date, this is a clean round-trip;
 *   otherwise the wall clock is preserved and a structured warning is returned via
 *   the by-ref `$warning` argument (mode = `preserve_wall_clock`).
 * - TZ-less wall clock, e.g. `2026-05-02 09:00:00` — interpreted as destination-local.
 * - Empty string — returns 0.
 *
 * Rejects:
 * - Bare numeric epochs (ambiguous given the fake-UTC storage convention).
 * - Anything else strtotime cannot parse.
 *
 * @since 3.6.0
 *
 * @param string     $value   Raw CSV value.
 * @param array|null $warning Out-param populated when offsets disagree with site.
 * @return int|WP_Error Fake-UTC timestamp, 0 for empty, or WP_Error on invalid input.
 */
function wc_bookings_parse_csv_datetime( $value, &$warning = null ) {
	$warning = null;

	if ( null === $value || '' === $value ) {
		return 0;
	}

	$value = (string) $value;

	if ( is_numeric( $value ) ) {
		return new WP_Error(
			'wc_bookings_invalid_datetime',
			__( 'Numeric epoch values are ambiguous; provide an ISO 8601 datetime with offset (e.g. 2026-05-02T09:00:00-04:00).', 'woocommerce-bookings' ),
			array( 'value' => $value )
		);
	}

	$has_offset = (bool) preg_match( '/(?:Z|[+\-]\d{2}:?\d{2})$/', $value );
	$site_tz    = new DateTimeZone( wc_booking_get_timezone_string() );

	try {
		if ( $has_offset ) {
			$dt = new DateTimeImmutable( $value );
		} else {
			$dt = new DateTimeImmutable( $value, $site_tz );
		}
	} catch ( Exception $e ) {
		return new WP_Error(
			'wc_bookings_invalid_datetime',
			__( 'Could not parse the datetime value.', 'woocommerce-bookings' ),
			array( 'value' => $value )
		);
	}

	$site_offset  = $site_tz->getOffset( $dt );
	$value_offset = $dt->getOffset();

	if ( $has_offset && $value_offset !== $site_offset ) {
		$warning = array(
			'code'        => 'offset_mismatch',
			'csv_offset'  => $dt->format( 'P' ),
			'site_offset' => $dt->setTimezone( $site_tz )->format( 'P' ),
			'wall_clock'  => $dt->format( 'Y-m-d\TH:i:s' ),
		);
	}

	/**
	 * Filter the timestamp mode used by the bookings CSV importer.
	 *
	 * `preserve_wall_clock` (default) keeps the same wall clock as the source CSV,
	 * which is correct for most cross-site imports. `preserve_instant` shifts the
	 * value to the destination's local time, preserving the absolute moment in time.
	 *
	 * @since 3.6.0
	 *
	 * @param string $mode Either `preserve_wall_clock` or `preserve_instant`.
	 */
	$mode       = apply_filters( 'woocommerce_bookings_importer_timestamp_mode', 'preserve_wall_clock' );
	$wall_clock = ( 'preserve_instant' === $mode )
		? $dt->setTimezone( $site_tz )->format( 'Y-m-d H:i:s' )
		: $dt->format( 'Y-m-d H:i:s' );

	return strtotime( $wall_clock );
}

/**
 * Get bookable product resources.
 *
 * @param int $product_id product ID.
 *
 * @return array Resources objects list.
 */
function wc_booking_get_product_resources( $product_id ) {
	wc_deprecated_function( __FUNCTION__, '1.15.0', 'WC_Product_Booking->get_resources()' );

	$booking_product = get_wc_product_booking( $product_id );
	return $booking_product->get_resources();
}

/**
 * Get bookable product resource by ID.
 *
 * @param int $product_id product ID.
 * @param int $resource_id resource ID
 *
 * @return array Resources object.
 */
function wc_booking_get_product_resource( $product_id, $resource_id ) {
	wc_deprecated_function( __FUNCTION__, '1.15.0', 'WC_Product_Booking->get_resource()' );

	$booking_product = get_wc_product_booking( $product_id );
	return $booking_product->get_resource( $resource_id );
}

/**
 * Output how priority rules work with full HTML.
 */
function wc_booking_rules_explanation_html() {
?>
	<p class="wc-booking-availability-description">
		<span class="dashicons dashicons-info"></span>
		<span>
			<?php
			$priority_rules = get_wc_booking_rules_explanation();
			foreach ( $priority_rules as $priority_rule ) {
				echo esc_html( $priority_rule ) . '<br>';
			}
			?>
		</span>
	</p>
<?php
}

/**
 * Get descriptive text on how priority rules work.
 *
 * @since 1.9.10
 * @return array
 */
function get_wc_booking_rules_explanation() {
	return array(
		esc_html__( 'A lower priority rule will always override a higher priority rule e.g. 9 overrides 10.', 'woocommerce-bookings' ),
		esc_html__( 'If there are multiple rules with the same priority, then the order of the rules is considered. In this case the rule with the higher order will take precedence over the rule with the lower order.', 'woocommerce-bookings' ),
	);
}

/**
 * get_wc_booking_priority_explanation.
 *
 * @return string
 */
function get_wc_booking_priority_explanation() {
	return __( 'Rules with lower priority numbers will override rules with a higher priority (e.g. 9 overrides 10 ). Global rules take priority over product rules which take priority over resource rules. By using priority numbers you can execute rules in different orders.', 'woocommerce-bookings' );
}

/**
 * Get the min timestamp that is bookable based on settings.
 *
 * If $today is the current day, offset starts from NOW rather than midnight.
 *
 * @param int $today Current timestamp, defaults to now.
 * @param int $offset
 * @param string $unit
 * @return int
 */
function wc_bookings_get_min_timestamp_for_day( $date, $offset, $unit ) {
	$timestamp = $date;

	$now = current_time( 'timestamp' );
	$is_today     = date( 'y-m-d', $date ) === date( 'y-m-d', $now );

	if ( $is_today || empty( $date ) ) {
		$timestamp = strtotime( "midnight +{$offset} {$unit}", $now );
	}
	return $timestamp;
}

/**
 * Give this function a booking or resource ID, and a range of dates and get back
 * how many places are available for the requested quantity of bookings for all blocks within those dates.
 *
 * Replaces the WC_Product_Booking::get_available_bookings method.
 *
 * @param  WC_Product_Booking | integer $bookable_product Can be a product object or a booking prouct ID.
 * @param  integer $start_date
 * @param  integer $end_date
 * @param  integer|null optional $resource_id
 * @param  integer $qty
 * @param  array   $intervals
 * @return array|int|boolean|WP_Error False if no places/blocks are available or the dates are invalid.
 */
function wc_bookings_get_total_available_bookings_for_range( $bookable_product, $start_date, $end_date, $resource_id = null, $qty = 1, $intervals = array() ) {
	// alter the end date to limit it to go up to one slot if the setting is enabled
	if ( $bookable_product->get_check_start_block_only() ) {
		$end_date = strtotime( '+ ' . $bookable_product->get_duration() . ' ' . $bookable_product->get_duration_unit(), $start_date );
	}
	// Check the date is not in the past
	if ( date( 'Ymd', $start_date ) < date( 'Ymd', current_time( 'timestamp' ) ) ) {
		return false;
	}
	// Check we have a resource if needed
	$booking_resource = $resource_id ? $bookable_product->get_resource( $resource_id ) : null;
	if ( $bookable_product->has_resources() && ! is_numeric( $resource_id ) ) {
		return false;
	}
	$min_date   = $bookable_product->get_min_date();
	$max_date   = $bookable_product->get_max_date();
	$check_from = strtotime( "midnight +{$min_date['value']} {$min_date['unit']}", current_time( 'timestamp' ) );
	$check_to   = strtotime( "+{$max_date['value']} {$max_date['unit']}", current_time( 'timestamp' ) );
	// Min max checks
	if ( 'month' === $bookable_product->get_duration_unit() ) {
		$check_to = strtotime( 'midnight', strtotime( date( 'Y-m-t', $check_to ) ) );
	}
	if ( $end_date < $check_from || $start_date > $check_to ) {
		return false;
	}
	// Get availability of each resource - no resource has been chosen yet
	if ( $bookable_product->has_resources() && ! $resource_id ) {
		return $bookable_product->get_all_resources_availability( $start_date, $end_date, $qty );
	} else {
		// If we are checking for bookings for a specific resource, or have none.
		$check_date     = $start_date;
		if ( in_array( $bookable_product->get_duration_unit(), array( 'minute', 'hour' ) ) ) {
			if ( ! $bookable_product->check_availability_rules_against_time( $start_date, $end_date, $resource_id ) ) {
				return false;
			}
		} else {
			while ( $check_date < $end_date ) {
				if ( ! $bookable_product->check_availability_rules_against_date( $check_date, $resource_id ) ) {
					return false;
				}
				if ( $bookable_product->get_check_start_block_only() ) {
					break; // Only need to check first day
				}
				$check_date = strtotime( '+1 day', $check_date );
			}

			// Also check the end time because it was skipped above, in while loop.
			// This is helpful only for accommodation bookable products when the
			// end time is on the next day / not on the same day.
			if ( ! $bookable_product->get_check_start_block_only()
			     && ! $bookable_product->check_availability_rules_against_date( $end_date, $resource_id ) ) {
				return false;
			}
		}
		// Get blocks availability
		return $bookable_product->get_blocks_availability( $start_date, $end_date, $qty, $booking_resource, $intervals );
	}
}

/**
 * (via Action Scheduler) Find available and booked blocks for specific resources (if any) and return them as array.
 *
 * @param  int $product_id
 * @param  integer $min_date The starting date for the set of blocks
 * @param  integer $max_date The ending date for the set of blocks
 * @param  integer $timezone_offset Timezone offest.
 * @return void
 *
 * @version  1.15.70
 */
function wc_bookings_get_time_slots_scheduled( $product_id, $min_date, $max_date, $timezone_offset ) {

	// Add meta to mark the event is started.
	$transient_name = 'book_ts_' . md5( http_build_query( array( $product_id, 0, $min_date, $max_date, false ) ) );
	update_post_meta( $product_id, 'cache_update_started-' . $transient_name, true );

	// Delete transient, we can't delete it for a specific product,
	// because the same resource may be in use by other products,
	// so we also need to delete their transients, perhaps a work for the future.
	// right now deleting all transients.
	WC_Bookings_Cache::delete_booking_slots_transient();

	// Re-call the function to generate the latest time slots.
	WC_Bookings_Controller::find_booked_day_blocks( $product_id, $min_date, $max_date, 'Y-n-j', $timezone_offset, array(), 'action-scheduler' );
}

add_action( 'wc-booking-scheduled-update-availability', 'wc_bookings_get_time_slots_scheduled', 10, 8 );

/**
 * Retrieve available booking time slots for a bookable product.
 *
 * @since 1.10.5
 *
 * @param  WC_Product_Booking $bookable_product The bookable product.
 * @param  array              $blocks           Array of block start timestamps.
 * @param  array              $intervals        Optional. Array containing [interval, base_interval]. If empty, defaults are derived from product duration.
 * @param  int                $resource_id      Optional. Resource ID to check. Defaults to 0 (no specific resource).
 * @param  int                $from             Optional. Range start timestamp. Defaults to 0.
 * @param  int                $to               Optional. Range end timestamp. Defaults to 0.
 * @param  bool               $include_sold_out Whether to include sold-out blocks in the results. Defaults to false.
 *
 * @return array {
 *     Associative array of available time slots, keyed by block timestamp.
 *
 *     @type array $timestamp {
 *         Availability details for a block.
 *
 *         @type int   $booked    Number of slots already booked.
 *         @type int   $available Number of slots still available.
 *         @type array $resources Map of resource IDs to available quantities.
 *     }
 *     @type bool  $old_availability True if the response contains cached/stale availability.
 * }
 */
function wc_bookings_get_time_slots( $bookable_product, $blocks, $intervals = array(), $resource_id = 0, $from = 0, $to = 0, $include_sold_out = false ) {
	$arg_list                     = func_get_args();
	$product_id                   = $bookable_product->get_id();
	$transient_name               = 'book_ts_' . md5( http_build_query( array( $bookable_product->get_id(), $resource_id, $from, $to, $include_sold_out ) ) );
	$booking_slots_transient_keys = array_filter( (array) WC_Bookings_Cache::get( 'booking_slots_transient_keys' ) );
	$available_slots              = WC_Bookings_Cache::get( $transient_name );

	// Should we serve the old availability until the action scheduler runs or not.
	$old_availability_served = false;

	// Action Scheduler Event exists or not. Set to false by default.
	$clear_cache_event      = false;
	$cache_update_scheduled = get_post_meta( $product_id, 'cache_update_scheduled-' . $transient_name );

	// If event is created show the old availability (i.e. from transient) to the user.
	// The $arg_list[7] is $timezone_offset, this is required to check event's existence.
	// The $arg_list[8] is $source, this is to know if the request is coming from the 'user'
	// or this is a call from the event itself (i.e. 'action-scheduler').
	if ( $cache_update_scheduled && isset( $arg_list[7] ) && isset( $arg_list[8] ) && 'user' === $arg_list[8] ) {
		// If the action scheduler event is not set,
		// or it is not running right now (i.e. 'cache_update_started-..' meta does not exist),
		// force clear the transient now. Otherwise, keep serving the old availability.
		$clear_cache_event    = as_has_scheduled_action( 'wc-booking-scheduled-update-availability', array( $product_id, $from, $to, $arg_list[7] ) );
		$cache_update_started = get_post_meta( $product_id, 'cache_update_started-' . $transient_name, true );
		if ( ! $clear_cache_event && ! $cache_update_started ) {
			update_post_meta( $product_id, 'cache_update_started-' . $transient_name, true );
			WC_Bookings_Cache::delete_booking_slots_transient();
		} else {
			// Old availability should be served until the action is fired.
			$old_availability_served = true;
		}
	}

	// If we have to serve the old availability, but we don't have availability slots in transient,
	// Let's see if we have 'prev-availability' transient available (that is created by the event scheduler).
	if ( $old_availability_served && ! $available_slots ) {
		// If not available, set it to `false` to prevent `Illegal string offset` issue.
		$available_slots = WC_Bookings_Cache::get( 'prev-availability-' . $transient_name ) ?: false;
	}

	if ( ! isset( $booking_slots_transient_keys[ $bookable_product->get_id() ] ) ) {
		$booking_slots_transient_keys[ $bookable_product->get_id() ] = array();
	}

	if ( ! in_array( $transient_name, $booking_slots_transient_keys[ $bookable_product->get_id() ] ) ) {
		$booking_slots_transient_keys[ $bookable_product->get_id() ][] = $transient_name;
		// Give array of keys a long ttl because if it expires we won't be able to flush the keys when needed.
		// We can't use 0 to never expire because then WordPress will autoload the option on every page.
		WC_Bookings_Cache::set( 'booking_slots_transient_keys', $booking_slots_transient_keys, YEAR_IN_SECONDS );
	}

	if ( false === $available_slots ) {
		if ( empty( $intervals ) ) {
			$default_interval = 'hour' === $bookable_product->get_duration_unit() ? $bookable_product->get_duration() * 60 : $bookable_product->get_duration();
			$interval         = $bookable_product->get_min_duration() * $default_interval;
			$intervals        = array( $interval, $default_interval );
		}

		list( $interval, $base_interval ) = $intervals;
		$interval = $bookable_product->get_check_start_block_only() ? $base_interval : $interval;

		if ( ! $include_sold_out ) {
			$blocks   = $bookable_product->get_available_blocks( array(
				'blocks'      => $blocks,
				'intervals'   => $intervals,
				'resource_id' => $resource_id,
				'from'        => $from,
				'to'          => $to,
			) );
		}

		$booking_resource = $resource_id ? $bookable_product->get_resource( $resource_id ) : null;
		$available_slots  = array();
		$has_qty          = is_object( $booking_resource ) ? $booking_resource->has_qty() : false;
		$has_resources    = $bookable_product->has_resources();

		/**
		 * Determine if a simple or extensive availability search should be performed.
		 *
		 * Do not use simple search if:
		 * - using automatic resource assignment,
		 * - using monthly duration unit,
		 * - fetching out of stock blocks.
		 *
		 * @since 3.0.0
		 *
		 * @param bool       $use_extensive_search
		 * @param WC_Product $bookable_product
		 * @param int        $resource_id
		 * @param bool       $include_sold_out
		 */
		$use_extensive_search = apply_filters( 'woocommerce_bookings_use_extensive_time_slots_check', true, $bookable_product, $resource_id, $include_sold_out );

		if ( ! $use_extensive_search ) {
			foreach ( $blocks as $block => $booked_slots ) {
				$resources = array();

				$available_qty             = $has_qty ? $booking_resource->get_qty() : $bookable_product->get_qty();
				$resources[ $resource_id ] = $available_qty - $booked_slots;

				$available_slots[ $block ] = array(
					'booked'    => $booked_slots,
					'available' => $available_qty - $booked_slots,
					'resources' => $resources,
				);
			}
		} else {
			/*
			 * The calculations below are performed using array of minutes for the following reason:
			 *
			 * We are trying to figure-out resource and availability quote for the booking $block.
			 * In order to do that we have to check all other bookings that are already booked and
			 * take time overlapping the $block. Inconveniently bookings in $existing_bookings may come
			 * from products that are different than our $bookable_product.
			 * This means that their duration and therefore, start and end times
			 * may not be aligned with our product $block and $interval. We can't because of that
			 * check quote and resources 1 to 1 but we need to look at individual minutes.
			 */
			$existing_bookings = WC_Booking_Data_Store::get_all_existing_bookings( $bookable_product, $from, $to );

			foreach ( $blocks as $block => $booked_slots ) {
				$resources = array();

				// Figure out how much qty have, either based on combined resource quantity,
				// single resource, or just product.
				if ( $has_resources && ( ! is_a( $booking_resource, 'WC_Product_Booking_Resource' ) || ! $has_qty ) ) {
					$available_qty = 0;

					foreach ( $bookable_product->get_resources() as $resource ) {

						// Only include if it is available for this selection.
						if ( ! WC_Product_Booking_Rule_Manager::check_availability_rules_against_date( $bookable_product, $resource->get_id(), $block ) ) {
							continue;
						}

						if ( in_array( $bookable_product->get_duration_unit(), array( 'minute', 'hour' ), true ) && ! $bookable_product->check_availability_rules_against_time( $block, strtotime( "+{$interval} minutes", $block ), $resource->get_id() ) ) {
							continue;
						}

						$qty                              = $resource->get_qty();
						$available_qty                   += $qty;
						$resources[ $resource->get_id() ] = $qty;
					}
				} elseif ( $has_resources && $has_qty ) {
					// Only include if it is available for this selection. We set this block to be bookable by default, unless some of the rules apply.
					if ( ! $bookable_product->check_availability_rules_against_time( $block, strtotime( "+{$interval} minutes", $block ), $booking_resource->get_id() ) ) {
						continue;
					}

					$qty                                      = $booking_resource->get_qty();
					$available_qty                            = $qty;
					$resources[ $booking_resource->get_id() ] = $qty;
				} else {
					$available_qty = $bookable_product->get_qty();
					$resources[0]  = $bookable_product->get_qty();
				}

				$qty_booked_in_block = 0;

				if ( in_array( $bookable_product->get_duration_unit(), array( 'minute', 'hour' ), true ) ) {
					// Duration in minutes for intra-day duration.
					$inteval_in_minutes = $interval;
				} else {
					// Maximum 1 full day for "fixed blocks of N days" duration.
					$inteval_in_minutes = DAY_IN_SECONDS / MINUTE_IN_SECONDS;
				}

				// Prepare ( array ) of minutes we are looking at.
				$block_minutes_array = wc_bookings_get_block_minutes_array( $block, $inteval_in_minutes );

				// Spread resources and booked quote using minutes array.
				$resources           = array_map(
					function ( $r ) use ( $block_minutes_array ) {
						return array_fill_keys( array_keys( $block_minutes_array ), $r );
					},
					$resources
				);
				$qty_booked_in_block = $block_minutes_array;

				if ( ! empty( $existing_bookings ) ) {
					foreach ( $existing_bookings as $existing_booking ) {
						if ( $existing_booking->is_intersecting_block( $block, strtotime( "+{$inteval_in_minutes} minutes", $block ) ) ) {
							$existing_booking_product = $existing_booking->get_product();
							$qty_to_add               = $existing_booking_product->has_person_qty_multiplier() ? max( 1, array_sum( $existing_booking->get_persons() ) ) : 1;
							// Default resource in case we don't have anything in product or existing bookings.
							$res = 0;
							if ( $has_resources ) {
								if ( $existing_booking->get_resource_id() === absint( $resource_id ) ) {
									// Include the quantity to subtract if an existing booking matches the selected resource id.
									$res = $resource_id;
								} elseif ( ( is_null( $booking_resource ) || ! $has_qty ) && $existing_booking->get_resource() ) {
									// Include the quantity to subtract if the resource is auto selected (null/resource id empty)
									// but the existing booking includes a resource.
									$res = $existing_booking->get_resource_id();
								} else {
									// We have resource but the existing booking resource does not overlap so we don't need to take it into consideration.
									continue;
								}
							}

							$existing_booking_block      = $existing_booking->get_start_cached();
							$existing_booking_interval   = ceil( ( $existing_booking->get_end_cached() - $existing_booking_block ) / MINUTE_IN_SECONDS );
							$booking_block_minutes_array = wc_bookings_get_block_minutes_array( $existing_booking_block, $existing_booking_interval );
							$qty_booked_in_block         = wc_bookings_add_at_intersection( $qty_booked_in_block, $booking_block_minutes_array, $qty_to_add );
							$res                         = $has_resources ? $existing_booking->get_resource_id() : 0;
							$resources[ $res ]           = ( isset( $resources[ $res ] ) ? $resources[ $res ] : $block_minutes_array );
							$resources[ $res ]           = wc_bookings_add_at_intersection( $resources[ $res ], $booking_block_minutes_array, - $qty_to_add );
						}
					}
				}

				// The minute with minimal resource value is the actual resource available from the perspective of the whole $block.
				$resources = array_map(
					function ( $r ) {
						return min( $r );
					},
					$resources
				);
				// The minute with maximal booked quota value is the actual booked quota from the perspective of the whole $block.
				$qty_booked_in_block = max( $qty_booked_in_block );

				$available_slots[ $block ] = array(
					'booked'    => $qty_booked_in_block,
					'available' => $available_qty - $qty_booked_in_block,
					'resources' => $resources,
				);
			}
		}

		WC_Bookings_Cache::set( $transient_name, $available_slots );

		// Delete cache schedule tracking metas.
		delete_post_meta( $product_id, 'cache_update_scheduled-' . $transient_name );
		delete_post_meta( $product_id, 'cache_update_started-' . $transient_name );

		// Delete the backup of previous availability.
		WC_Bookings_Cache::delete( 'prev-availability-' . $transient_name );

		// Reset Flag.
		$old_availability_served = false;

		// Delete if the action is still exists. We don't need it anymore as the availability is generated!
		// This is possible when the cache is cleared from somewhere else or by another action.
		// Note: The $arg_list[7] is already set if `$clear_cache_event` is set. No need to check with `isset()`.
		if ( $clear_cache_event ) {
			as_unschedule_action( 'wc-booking-scheduled-update-availability', array( $product_id, $from, $to, $arg_list[7] ) );
		}
	}

	// Return `old_availability` flag to the browser, so we can display a message
	// to the users that the old availability is being served to them or the latest one is being served.
	if ( $old_availability_served ) {
		$available_slots['old_availability'] = $old_availability_served;
	}

	$args = array(
		'blocks'           => $blocks,
		'intervals'        => $intervals,
		'resource_id'      => $resource_id,
		'from'             => $from,
		'to'               => $to,
		'include_sold_out' => $include_sold_out,
	);
	return apply_filters( 'woocommerce_bookings_filter_time_slots', $available_slots, $bookable_product, $args );
}

/**
 * Take two arrays, check where they intersect,
 * add value to all elements in array1 from the intersection.
 *
 * @since 1.15.13
 *
 * @param array   $array1 First array.
 * @param array   $array1 Second array.
 * @param integer $value  Value to add to the intersection.
 * @return array          First array updated with $value at intersection.
 */
function wc_bookings_add_at_intersection( $array1, $array2, $value ) {
	$timestamps = array_keys( array_intersect_key( $array1, $array2 ) );
	foreach ( $timestamps as $timestamp ) {
		$array1[ $timestamp ] += $value;
	}
	return $array1;
}

/**
 * Generate a minute-level map for a booking block.
 *
 * Creates an associative array where each key is a Unix timestamp for a
 * specific minute within the block, and each value is initialized to `0`.
 * Used to track availability and overlaps at the granularity of minutes.
 *
 * Example:
 *   Input:  $block = 1735689600 (Jan 1, 2025 00:00), $interval = 3
 *   Output: {
 *     1735689600 => 0, // 00:00
 *     1735689660 => 0, // 00:01
 *     1735689720 => 0, // 00:02
 *   }
 *
 * @since 1.15.13
 *
 * @param  int $block    Block start timestamp.
 * @param  int $interval Length of the block in minutes.
 *
 * @return array<int,int> Associative array keyed by minute timestamp,
 *                        each initialized with value `0`.
 */
function wc_bookings_get_block_minutes_array( $block, $interval ) {
	$block_array = [];
	for( $i = 0; $i < $interval; $i++ ) {
		$block_array[ $block + ( $i * MINUTE_IN_SECONDS ) ] = 0;
	}
	return $block_array;
}

/**
 * Builds the HTML to display the start time for hours/minutes.
 *
 * @since 1.13.0
 * @since 1.15.0 Deprecated
 * @param \WC_Product_Booking $bookable_product
 * @param  array  $blocks
 * @param  array  $intervals
 * @param  integer $resource_id
 * @param  integer $from The starting date for the set of blocks
 * @param  integer $to
 * @param  array $available_blocks
 * @return string
 *
 */
function wc_bookings_get_start_time_html( $bookable_product, $blocks, $intervals = array(), $resource_id = 0, $from = 0, $to = 0 ) {
	wc_deprecated_function( __FUNCTION__, '1.15.0', 'WC_Booking_Form::get_start_time_html()' );

	$booking_form = new WC_Booking_form( $bookable_product );

	return $booking_form->get_start_time_html( $blocks, $intervals, $resource_id, $from, $to );
}

/**
 * Builds the data to display the end time for hours/minutes.
 *
 * @since 1.13.0
 * @param \WC_Product_Booking $bookable_product
 * @param  array  $blocks
 * @param  string $start_date_time Date of the start time.
 * @param  array  $intervals
 * @param  integer $resource_id
 * @param  integer $from The starting date for the set of blocks
 * @param  integer $to
 * @param  bool    $check Whether to just check if there's any data at all.
 * @return array
 *
 */
function wc_bookings_get_end_times( $bookable_product, $blocks, $start_date_time = '', $intervals = array(), $resource_id = 0, $from = 0, $to = 0, $check = false ) {
	wc_deprecated_function( __FUNCTION__, '1.15.0', 'WC_Booking_Form::get_end_times()' );

	$booking_form = new WC_Booking_Form( $bookable_product );

	return $booking_form->get_end_times( $blocks, $start_date_time, $intervals, $resource_id, $from, $to, $check );
}

/**
 * Renders the HTML to display the end time for hours/minutes.
 *
 * @since 1.13.0
 * @since 1.15.0 Deprecated
 * @param \WC_Product_Booking $bookable_product
 * @param  array  $blocks
 * @param  string $start_date_time Date of the start time.
 * @param  array  $intervals
 * @param  integer $resource_id
 * @param  integer $from The starting date for the set of blocks
 * @param  integer $to
 * @return string
 *
 */
function wc_bookings_get_end_time_html( $bookable_product, $blocks, $start_date_time = '', $intervals = array(), $resource_id = 0, $from = 0, $to = 0 ) {
	wc_deprecated_function( __FUNCTION__, '1.15.0', 'WC_Booking_Form::get_end_time_html()' );

	$booking_form = new WC_Booking_Form( $bookable_product );

	return $booking_form->get_end_time_html( $blocks, $start_date_time, $intervals, $resource_id, $from, $to );
}

/**
 * Find available blocks and return HTML for the user to choose a block. Used in class-wc-bookings-ajax.php.
 *
 * @param \WC_Product_Booking $bookable_product
 * @param  array  $blocks
 * @param  array  $intervals
 * @param  integer $resource_id
 * @param  integer $from The starting date for the set of blocks
 * @param  integer $to
 * @return string
 *
 * @version  1.10.7
 */
function wc_bookings_get_time_slots_html( $bookable_product, $blocks, $intervals = array(), $resource_id = 0, $from = 0, $to = 0 ) {
	wc_deprecated_function( __FUNCTION__, '1.15.0', 'WC_Booking_Form::get_time_slots_html()' );

	$booking_form = new WC_Booking_Form( $bookable_product );

	return $booking_form->get_time_slots_html( $blocks, $intervals, $resource_id, $from, $to );
}

function get_time_as_iso8601( $timestamp ) {
	$timezone = wc_booking_get_timezone_string();
	$server_time = new DateTime( date( 'Y-m-d\TH:i:s', $timestamp ), new DateTimeZone( $timezone ) );

	return $server_time->format( DateTime::ISO8601 );
}

/**
 * Find available blocks and return HTML for the user to choose a block. Used in class-wc-bookings-ajax.php.
 *
 * @deprecated since 1.10.0
 * @param \WC_Product_Booking $bookable_product
 * @param  array  $blocks
 * @param  array  $intervals
 * @param  integer $resource_id
 * @param  string  $from The starting date for the set of blocks
 * @return string
 */
function wc_bookings_available_blocks_html( $bookable_product, $blocks, $intervals = array(), $resource_id = 0, $from = '' ) {
	_deprecated_function( 'Please use wc_bookings_get_time_slots_html', 'Bookings: 1.10.0' );
	return wc_bookings_get_time_slots_html( $bookable_product, $blocks, $intervals, $resource_id, $from );
}

/**
 * Summary of booking data for admin and checkout.
 *
 * @version 1.15.44
 *
 * @param  WC_Booking $booking
 * @param  bool       $is_admin To determine if this is being called in admin or not.
 */
function wc_bookings_get_summary_list( $booking, $is_admin = false ) {
	$product  = $booking->get_product();
	$resource = $booking->get_resource();
	$label    = $product && is_callable( array( $product, 'get_resource_label' ) ) && $product->get_resource_label() ? $product->get_resource_label() : __( 'Type', 'woocommerce-bookings' );

	$get_local_time = wc_should_convert_timezone( $booking );
	if ( strtotime( 'midnight', $booking->get_start() ) === strtotime( 'midnight', $booking->get_end() ) ) {
		$booking_date = sprintf(
			'<span class="booking-start-date" data-all-day="%1$s" data-timezone="%2$s">%3$s</span>',
			$booking->is_all_day() ? 'yes' : 'no',
			$booking->get_booking_timezone(),
			$booking->get_start_date( null, null, $get_local_time )
		);
	} else {
		$booking_date = sprintf(
			'<span class="booking-start-date" data-all-day="%1$s" data-timezone="%2$s">%3$s</span> - <span class="booking-end-date" data-all-day="%1$s" data-timezone="%2$s">%4$s</span>',
			$booking->is_all_day() ? 'yes' : 'no',
			$booking->get_booking_timezone(),
			$booking->get_start_date( null, null, $get_local_time ),
			$booking->get_end_date( null, null, $get_local_time )
		);
	}

	$template_args = array(
		'booking'          => $booking,
		'product'          => $product,
		'resource'         => $resource,
		'label'            => $label,
		'booking_date'     => $booking_date,
		'booking_timezone' => str_replace( '_', ' ', $booking->get_local_timezone() ),
		'is_admin'         => $is_admin,
	);

	wc_get_template( 'order/booking-summary-list.php', $template_args, 'woocommerce-bookings', WC_BOOKINGS_TEMPLATE_PATH );
}

/**
 * Converts a string (e.g. yes or no) to a bool.
 * @param  string $string
 * @return boolean
 */
function wc_bookings_string_to_bool( $string ) {
	if ( function_exists( 'wc_string_to_bool' ) ) {
		return wc_string_to_bool( $string );
	}
	return is_bool( $string ) ? $string : ( 'yes' === $string || 1 === $string || 'true' === $string || '1' === $string );
}

/**
 * @since 1.10.0
 * @param $minute
 * @param $check_date
 *
 * @return int
 */
function wc_booking_minute_to_time_stamp( $minute, $check_date ) {
	return strtotime( "+ $minute minutes", $check_date );
}

/**
 * Convert a timestamp into the minutes after 0:00
 *
 * @since 1.10.0
 * @param integer $timestamp
 * @return integer $minutes_after_midnight
 */
function wc_booking_time_stamp_to_minutes_after_midnight( $timestamp ) {
	$hour = absint( date( 'H', $timestamp ) );
	$min  = absint( date( 'i', $timestamp ) );
	return  $min + ( $hour * 60 );
}

/**
 * Get timezone offset in seconds.
 *
 * @since  1.10.3
 * @return float
 */
function wc_booking_timezone_offset() {
	$timezone = get_option( 'timezone_string' );
	if ( $timezone ) {
		$timezone_object = new DateTimeZone( $timezone );
		return $timezone_object->getOffset( new DateTime( 'now' ) );
	} else {
		return floatval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS;
	}
}

/**
 * Get the site timezone offset in seconds at a given timestamp.
 *
 * Resolves the offset at the given moment (not "now") so DST-observing zones land correctly.
 *
 * @since 3.6.0
 *
 * @param int $timestamp Unix timestamp.
 * @return float Offset in seconds.
 */
function wc_booking_timezone_offset_at( $timestamp ) {
	try {
		$dt = new DateTimeImmutable( gmdate( 'Y-m-d H:i:s', (int) $timestamp ), wp_timezone() );
	} catch ( Exception $e ) {
		return wc_booking_timezone_offset();
	}

	return $dt->getOffset();
}

/**
 * Determine whether Booking time should be converted to local time.
 *
 * @since  1.11.4
 * @return bool
 */
function wc_should_convert_timezone( $booking = null ) {
	if ( 'no' === WC_Bookings_Timezone_Settings::get( 'use_client_timezone' ) ) {
		return false;
	}

	// If we don't have a booking, just use the setting and return true
	if ( is_null( $booking ) ) {
		return true;
	}

	// If a Booking exists, make sure the local timezone is populated (does not happen for day duration e.g.)
	return ! empty( $booking->get_local_timezone() );
}

/**
 * Clear booking slots transient.
 *
 * In contexts where we have a product id, it will only delete the specific ones.
 * However, not all contexts will have a product id, e.g. Global Availability.
 *
 * @param  int|null $bookable_product_id
 * @since  1.13.12
 */
function delete_booking_slots_transient( $bookable_product_id = null ) {
	wc_deprecated_function( __FUNCTION__, '1.15.0', 'WC_Bookings_Cache::delete_booking_slots_transient()' );

	return WC_Bookings_Cache::delete_booking_slots_transient( $bookable_product_id );
}

/**
 * Renders a json object with a paginated availability set.
 *
 * @since 1.14.0
 */
function wc_bookings_paginated_availability( $availability, $page, $records_per_page ) {
	$records = array();

	if ( false === $page ) {
		$records = $availability;
	} else {
		$records = array_slice( $availability, ( $page - 1 ) * $records_per_page, $records_per_page );
	}
	$paginated_booking_slots = array(
		'records' => $records,
		'count' => count( $availability ),
	);

	return $paginated_booking_slots;
}

use DeviceDetector\DeviceDetector;
use DeviceDetector\Parser\Device\DeviceParserAbstract;

/**
 * Parse and normalize booking data from posted values.
 *
 * @since 1.15.0
 *
 * @param  array              $posted  Posted booking data. If empty, falls back to $_POST {
 *     wc_bookings_field_start_date_year
 *     wc_bookings_field_start_date_month
 *     wc_bookings_field_start_date_day
 *     wc_bookings_field_start_date_yearmonth (Used, for whole month selection; overrides the specific day fields)
 *     wc_bookings_field_start_date_time (Optional; only if product duration requires time; overrides the specific day fields)
 *     wc_bookings_field_resource (Only when Product has resources)
 *     wc_bookings_field_duration (Optional)
 *     wc_bookings_field_persons_<person_type_id> or wc_bookings_field_persons
 *     wc_bookings_field_start_date_local_timezone (Optional)
 * }.
 * @param  WC_Product_Booking $product The booking product object.
 *
 * @return array {
 *     Normalized booking data.
 *
 *     @type int    $_year            The booking start year.
 *     @type int    $_month           The booking start month.
 *     @type int    $_day             The booking start day.
 *     @type string $_date            The raw start date in Y-m-d format.
 *     @type string $date             Formatted start date (localized).
 *     @type string $_time            The raw start time in G:i format.
 *     @type string $time             Formatted start time (localized).
 *     @type int    $_qty             Quantity being booked.
 *     @type array  $_persons         Map of person type IDs to quantities.
 *     @type string $duration         Human-readable booking duration.
 *     @type int    $_duration        Numeric booking duration (customer-entered or fixed).
 *     @type string $_duration_unit   Duration unit (month, day, hour, minute, night).
 *     @type int    $_start_date      Start timestamp.
 *     @type int    $_end_date        End timestamp.
 *     @type int    $_all_day         Whether the booking spans the whole day (1) or not (0).
 *     @type int    $_resource_id     Assigned resource ID (0 if none).
 *     @type string $type             Resource type name, if applicable.
 *     @type string $_local_timezone  Client-provided local timezone.
 * }
 */
function wc_bookings_get_posted_data( $posted, $product ) {
	if ( empty( $posted ) ) {
		$posted = $_POST;
	}

	$data = array(
		'_year'    => '',
		'_month'   => '',
		'_day'     => '',
		'_persons' => array(),
	);

	// Get date fields (y, m, d)
	if ( ! empty( $posted['wc_bookings_field_start_date_year'] ) && ! empty( $posted['wc_bookings_field_start_date_month'] ) && ! empty( $posted['wc_bookings_field_start_date_day'] ) ) {
		$data['_year']  = absint( $posted['wc_bookings_field_start_date_year'] );
		$data['_year']  = $data['_year'] ? $data['_year'] : date( 'Y' );
		$data['_month'] = absint( $posted['wc_bookings_field_start_date_month'] );
		$data['_day']   = absint( $posted['wc_bookings_field_start_date_day'] );
		$data['_date']  = $data['_year'] . '-' . $data['_month'] . '-' . $data['_day'];
		$data['date']   = date_i18n( wc_bookings_date_format(), strtotime( $data['_date'] ) );
	}

	// Get year month field
	if ( ! empty( $posted['wc_bookings_field_start_date_yearmonth'] ) ) {
		$yearmonth      = strtotime( $posted['wc_bookings_field_start_date_yearmonth'] . '-01' );
		$data['_year']  = absint( date( 'Y', $yearmonth ) );
		$data['_month'] = absint( date( 'm', $yearmonth ) );
		$data['_day']   = 1;
		$data['_date']  = $data['_year'] . '-' . $data['_month'] . '-' . $data['_day'];
		$data['date']   = date_i18n( 'F Y', $yearmonth );
	}

	// Get time field
	if ( ! empty( $posted['wc_bookings_field_start_date_time'] ) ) {
		$date_time      = new DateTime( $posted['wc_bookings_field_start_date_time'] ); // Contains ISO 8061 formatted datetime
		$data['_year']  = $date_time->format( 'Y' );
		$data['_month'] = $date_time->format( 'm' );
		$data['_day']   = $date_time->format( 'd' );
		$data['_date']  = $data['_year'] . '-' . $data['_month'] . '-' . $data['_day'];
		$data['date']   = date_i18n( wc_bookings_date_format(), strtotime( $data['_date'] ) );
		$data['_time']  = $date_time->format( 'G:i' );
		$data['time']   = date_i18n( wc_bookings_time_format(), strtotime( "{$data['_year']}-{$data['_month']}-{$data['_day']} {$data['_time']}" ) );
	} else {
		$data['_time']  = '';
	}

	// Quantity being booked
	$data['_qty'] = 1;

	// Work out persons
	if ( $product->has_persons() ) {
		if ( $product->has_person_types() ) {
			$person_types = $product->get_person_types();

			foreach ( $person_types as $person_type ) {
				if ( isset( $posted[ 'wc_bookings_field_persons_' . $person_type->ID ] )
					&& absint( $posted[ 'wc_bookings_field_persons_' . $person_type->ID ] ) > 0 ) {
					$data[ $person_type->post_title ]     = absint( $posted[ 'wc_bookings_field_persons_' . $person_type->ID ] );
					$data['_persons'][ $person_type->ID ] = $data[ $person_type->post_title ];
				}
			}
		} elseif ( isset( $posted['wc_bookings_field_persons'] ) ) {
			$data[ __( 'Persons', 'woocommerce-bookings' ) ] = absint( $posted['wc_bookings_field_persons'] );
			$data['_persons'][0]                             = absint( $posted['wc_bookings_field_persons'] );
		}

		if ( $product->get_has_person_qty_multiplier() ) {
			$data['_qty'] = array_sum( $data['_persons'] );
		}
	}

	// Duration
	if ( 'customer' == $product->get_duration_type() ) {
		$booking_duration       = isset( $posted['wc_bookings_field_duration'] ) ? max( 0, absint( $posted['wc_bookings_field_duration'] ) ) : 0;
		$booking_duration_unit  = $product->get_duration_unit();

		$data['_duration_unit'] = $booking_duration_unit;
		$data['_duration']      = $booking_duration;

		// Get the duration * block duration
		$total_duration = $booking_duration * $product->get_duration();

		// Nice formatted version
		switch ( $booking_duration_unit ) {
			case 'month':
				$data['duration'] = $total_duration . ' ' . _n( 'month', 'months', $total_duration, 'woocommerce-bookings' );
				break;
			case 'day':
				if ( $total_duration % 7 ) {
					$data['duration']  = $total_duration . ' ' . _n( 'day', 'days', $total_duration, 'woocommerce-bookings' );
				} else {
					$duration_in_weeks = ( $total_duration / 7 );
					$data['duration']  = $duration_in_weeks . ' ' . _n( 'week', 'weeks', $duration_in_weeks, 'woocommerce-bookings' );
				}
				break;
			case 'hour':
				$data['duration'] = $total_duration . ' ' . _n( 'hour', 'hours', $total_duration, 'woocommerce-bookings' );
				break;
			case 'minute':
				$data['duration'] = $total_duration . ' ' . _n( 'minute', 'minutes', $total_duration, 'woocommerce-bookings' );
				break;
			case 'night':
				$data['duration'] = $total_duration . ' ' . _n( 'night', 'nights', $total_duration, 'woocommerce-bookings' );
				break;
			default:
				$data['duration'] = $total_duration;
				break;
		}
	} else {
		// Fixed duration
		$booking_duration       = $product->get_duration();
		$booking_duration_unit  = $product->get_duration_unit();
		$data['_duration_unit'] = $booking_duration_unit;
		$total_duration         = $booking_duration;
	}

	// Work out start and end dates/times
	if ( ! empty( $data['_time'] ) ) {
		$data['_start_date'] = strtotime( "{$data['_year']}-{$data['_month']}-{$data['_day']} {$data['_time']}" );
		$data['_end_date']   = strtotime( "+{$total_duration} {$booking_duration_unit}", $data['_start_date'] );
		$data['_all_day']    = 0;
	} else {
		$data['_start_date'] = strtotime( "{$data['_year']}-{$data['_month']}-{$data['_day']}" );
		// We need the following calculation to not add extra days (see #2147)
		$data['_end_date']   = strtotime( "+{$total_duration} {$booking_duration_unit} - 1 second", $data['_start_date'] );
		$data['_all_day']    = 1;
	}

	// Get posted resource or assign one for the date range
	if ( $product->has_resources() ) {
		if ( $product->is_resource_assignment_type( 'customer' ) ) {
			$resource = $product->get_resource( isset( $posted['wc_bookings_field_resource'] ) ? absint( $posted['wc_bookings_field_resource'] ) : 0 );

			if ( $resource ) {
				$data['_resource_id'] = $resource->ID;
				$data['type']         = $resource->post_title;
			} else {
				$data['_resource_id'] = 0;
			}
		} else {
			// Assign an available resource automatically
			$available_bookings = wc_bookings_get_total_available_bookings_for_range( $product, $data['_start_date'], $data['_end_date'], 0, $data['_qty'] );

			if ( is_array( $available_bookings ) ) {
				$data['_resource_id'] = current( array_keys( $available_bookings ) );
				$data['type']         = get_the_title( current( array_keys( $available_bookings ) ) );
			}
		}
	}

	$data['_local_timezone'] = '';
	if ( ! empty( $posted['wc_bookings_field_start_date_local_timezone'] ) ) {
		$data['_local_timezone'] = $posted['wc_bookings_field_start_date_local_timezone'];
	}

	return apply_filters( 'woocommerce_booking_form_get_posted_data', $data, $product, $total_duration );
}

/**
 * Get an array of formatted time values.
 *
 * @since 1.15.0
 * @param  string $timestamp
 * @return array
 */
function wc_bookings_get_formatted_times( $timestamp ) {
	return array(
		'timestamp'   => $timestamp,
		'year'        => intval( date( 'Y', $timestamp ) ),
		'month'       => intval( date( 'n', $timestamp ) ),
		'day'         => intval( date( 'j', $timestamp ) ),
		'week'        => intval( date( 'W', $timestamp ) ),
		'day_of_week' => intval( date( 'N', $timestamp ) ),
		'time'        => date( 'YmdHi', $timestamp ),
	);
}

/**
 * Attempt to convert a date formatting string from PHP to Moment.
 *
 * @param string $format
 * @return string
 */
function wc_bookings_convert_to_moment_format( $format ) {
	$replacements = array(
		'd'   => 'DD',
		'D'   => 'ddd',
		'j'   => 'D',
		'l'   => 'dddd',
		'N'   => 'E',
		'S'   => 'o',
		'w'   => 'e',
		'z'   => 'DDD',
		'W'   => 'W',
		'F'   => 'MMMM',
		'm'   => 'MM',
		'M'   => 'MMM',
		'n'   => 'M',
		't'   => '', // no equivalent
		'L'   => '', // no equivalent
		'o'   => 'YYYY',
		'Y'   => 'YYYY',
		'y'   => 'YY',
		'a'   => 'a',
		'A'   => 'A',
		'B'   => '', // no equivalent
		'g'   => 'h',
		'G'   => 'H',
		'h'   => 'hh',
		'H'   => 'HH',
		'i'   => 'mm',
		's'   => 'ss',
		'u'   => 'SSS',
		'e'   => 'zz', // deprecated since version 1.6.0 of moment.js
		'I'   => '', // no equivalent
		'O'   => '', // no equivalent
		'P'   => '', // no equivalent
		'T'   => '', // no equivalent
		'Z'   => '', // no equivalent
		'c'   => '', // no equivalent
		'r'   => '', // no equivalent
		'U'   => 'X',
		'\\d' => '[d]',
		'\\D' => '[D]',
		'\\j' => '[j]',
		'\\l' => '[l]',
		'\\N' => '[N]',
		'\\S' => '[S]',
		'\\w' => '[w]',
		'\\z' => '[z]',
		'\\W' => '[W]',
		'\\F' => '[F]',
		'\\m' => '[m]',
		'\\M' => '[M]',
		'\\n' => '[n]',
		'\\t' => '[t]',
		'\\L' => '[L]',
		'\\o' => '[o]',
		'\\Y' => '[Y]',
		'\\y' => '[y]',
		'\\a' => '[a]',
		'\\A' => '[A]',
		'\\B' => '[B]',
		'\\g' => '[g]',
		'\\G' => '[G]',
		'\\h' => '[h]',
		'\\H' => '[H]',
		'\\i' => '[i]',
		'\\s' => '[s]',
		'\\u' => '[u]',
		'\\e' => '[e]',
		'\\I' => '[I]',
		'\\O' => '[O]',
		'\\P' => '[P]',
		'\\T' => '[T]',
		'\\Z' => '[Z]',
		'\\c' => '[c]',
		'\\r' => '[r]',
		'\\U' => '[U]',
	);

	return strtr( $format, $replacements );
}

/**
 * Return WP's time format, defaulting to a non-empty one if it is unset.
 *
 * @return string
 */
function wc_bookings_time_format() {
	return wc_time_format() ?: 'g:i a';
}

/**
 * Return WP's date format, defaulting to a non-empty one if it is unset.
 *
 * @return string
 */
function wc_bookings_date_format() {
	return wc_date_format() ?: 'F j, Y';
}

/**
 * Search bookings.
 *
 * @param  string $term Term to search.
 * @return array List of bookings ID.
 */
function wc_booking_search( $term ) {
	$data_store = WC_Data_Store::load( 'booking' );
	return $data_store->search_bookings( str_replace( 'Booking #', '', wc_clean( $term ) ) );
}

/**
 * Get script dependencies from wp-scripts generated asset file.
 *
 * @since 2.0.3
 *
 * @param string   $entry_point             The entry point name in the webpack config.
 * @param string[] $additional_dependencies Additional dependencies to add to the list.
 * @return string[] An array of dependencies.
 */
function wc_booking_get_script_dependencies( $entry_point, $additional_dependencies = array() ) {
	/*
	 * Prevent attempts at path traversal.
	 *
	 * As the directory structure of the dist folder is flat, it's possible to use
	 * the basename of the entry point to prevent path traversal.
	 */
	$entry_point = basename( $entry_point );
	$asset_file  = WC_BOOKINGS_PLUGIN_PATH . '/dist/' . $entry_point . '.asset.php';

	if ( ! file_exists( $asset_file ) ) {
		return array();
	}

	$asset_data = require $asset_file;
	return array_merge( $asset_data['dependencies'], $additional_dependencies );
}

/**
 * Maybe mark a completed booking as attended.
 *
 * @param WC_Booking $booking Booking object.
 * @return bool Whether to mark the booking as attended.
 */
function wc_bookings_maybe_mark_completed_bookings_as_attended( $booking ) {

	if ( ! is_a( $booking, 'WC_Booking' ) ) {
		return false;
	}

	/**
	 * Filter to set attendance status to attended when booking becomes complete.
	 *
	 * @since 3.1.0
	 *
	 * @param bool       $set_attended Whether to set attendance status to attended. Default false (unattended).
	 * @param int        $booking_id   Booking ID.
	 * @param WC_Booking $booking      Booking object.
	 */
	if ( $booking->get_attendance_status() === 'unattended' && apply_filters( 'woocommerce_booking_complete_set_attended', false, $booking->get_id(), $booking ) ) {
		return true;
	}

	return false;
}

/**
 * Get a formatted inline summary string for a booking.
 *
 * Returns a human-readable string combining booking ID, date, time, and optionally resource/team member.
 * Example with ID: "Booking #42 · Wednesday, January 8, 2026 at 12:00 pm with Marianne"
 * Example without ID: "Wednesday, January 8, 2026 at 12:00 pm with Marianne"
 *
 * @since 3.1.0
 *
 * @param WC_Booking $booking The booking object.
 * @param array      $args {
 *     Optional. Array of arguments.
 *
 *     @type bool   $include_booking_id  Whether to include the booking ID prefix. Default false.
 *     @type string $context             Context for the summary (e.g., 'email', 'admin', 'frontend'). Default 'frontend'.
 *     @type string $separator           Separator between parts. Default ' · '.
 *     @type bool   $use_local_timezone  Whether to use local timezone. Default null (auto-detect).
 * }
 * @return string The formatted booking summary string.
 */
function wc_bookings_get_inline_summary( WC_Booking $booking, array $args = array() ) {
	// Parse arguments with defaults.
	$args = wp_parse_args(
		$args,
		array(
			'context'            => 'frontend',
			'separator'          => ' · ',
			'include_booking_id' => false,
			'use_local_timezone' => null,
		)
	);

	$parts = array();

	// Booking ID part (optional).
	if ( $args['include_booking_id'] ) {
		$parts[] = sprintf(
			/* translators: %d: booking ID */
			__( 'Booking #%d', 'woocommerce-bookings' ),
			$booking->get_id()
		);
	}

	// Date and time part.
	$datetime_string = wc_bookings_get_inline_datetime_with_resource( $booking );
	if ( $datetime_string ) {
		$parts[] = $datetime_string;
	}

	$summary = implode( $args['separator'], $parts );

	/**
	 * Filter the booking inline summary output.
	 *
	 * @since 3.1.0
	 *
	 * @param string     $summary The formatted summary string.
	 * @param WC_Booking $booking The booking object.
	 * @param array      $args    The arguments passed to the function.
	 */
	return apply_filters( 'woocommerce_bookings_inline_summary', $summary, $booking, $args );
}

/**
 * Get a formatted inline date/time string for a booking with team member name.
 *
 * Returns a human-readable date and time string with team member name.
 * Example: "Wednesday, January 8, 2026 at 12:00 pm with Marianne"
 *
 * @since 3.5.0
 *
 * @param WC_Booking $booking The booking object.
 * @return string The formatted date/time string with team member name.
 */
function wc_bookings_get_inline_datetime_with_resource( $booking ) {
	// Date and time part.
	$datetime_string = wc_bookings_get_inline_datetime( $booking );
	if ( ! $datetime_string ) {
		return '';
	}

	// Append team member name if applicable.
	$resource_id = $booking->get_resource_id();
	$product_id  = $booking->get_product_id();
	$resource    = get_wc_product_booking_resource( $resource_id, $product_id );

	if ( $resource && is_a( $resource, 'WC_Product_Booking_Team_Member' ) ) {
		$datetime_string .= ' ' . sprintf(
			/* translators: %s: team member name */
			__( 'with %s', 'woocommerce-bookings' ),
			$resource->get_name()
		);
	}

	return $datetime_string;
}

/**
 * Get a formatted inline date/time string for a booking.
 *
 * Returns a human-readable date and time string.
 * Example: "Wednesday, January 8, 2026 at 12:00 pm"
 *
 * TODO: Add support for range bookings.
 *
 * @since 3.1.0
 *
 * @param WC_Booking $booking The booking object.
 * @return string The formatted date/time string.
 */
function wc_bookings_get_inline_datetime( $booking ) {
	$start_timestamp = $booking->get_start( 'view', wc_should_convert_timezone( $booking ) );
	if ( ! $start_timestamp ) {
		return '';
	}

	if ( $booking->is_all_day() ) {
		// All day booking: "Wednesday, January 8, 2026".
		return date_i18n( 'l, F j, Y', $start_timestamp );
	}

	// Timed booking: "Wednesday, January 8, 2026 at 12:00 pm".
	return sprintf(
		/* translators: 1: date (e.g., Wednesday, January 8, 2026), 2: time (e.g., 12:00 pm) */
		__( '%1$s at %2$s', 'woocommerce-bookings' ),
		date_i18n( 'l, F j, Y', $start_timestamp ),
		date_i18n( 'g:i a', $start_timestamp )
	);
}
