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

/**
 * Get an appointment object by ID.
 *
 * Creates and returns a WC_Appointment instance for the given appointment ID.
 * Returns false if the appointment cannot be loaded or an error occurs.
 *
 * @since 1.0.0
 *
 * @param int|string $id Optional. Appointment ID. Default empty string.
 *
 * @return WC_Appointment|false Appointment object on success, false on failure.
 *
 * @throws Exception If appointment data cannot be loaded (logged, not thrown).
 *
 * @example
 * // Get appointment by ID
 * $appointment = get_wc_appointment( 123 );
 * if ( $appointment ) {
 *     echo $appointment->get_start();
 * }
 */
function get_wc_appointment( $id = '' ) { // phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod.Found
	try {
		return new WC_Appointment( $id );
	} catch ( Exception $e ) {
		wc_get_logger()->error( $e->getMessage() );
		return false;
	}
}

/**
 * Get an appointable product object by ID.
 *
 * Creates and returns a WC_Product_Appointment instance for the given product ID.
 * Returns false if the product cannot be loaded, is not an appointment product, or an error occurs.
 *
 * @since 1.0.0
 *
 * @param int $id Optional. Product ID. Default 0.
 *
 * @return WC_Product_Appointment|false Product object on success, false on failure.
 *
 * @throws Exception If product data cannot be loaded (logged, not thrown).
 *
 * @example
 * // Get appointable product
 * $product = get_wc_product_appointment( 456 );
 * if ( $product ) {
 *     echo $product->get_duration();
 * }
 */
function get_wc_product_appointment( $id = 0 ) { // phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod.Found
	try {
		return new WC_Product_Appointment( (int) $id );
	} catch ( Exception $e ) {
		wc_get_logger()->error( $e->getMessage() );
		return false;
	}
}

/**
 * Get an availability rules object by ID.
 *
 * Creates and returns a WC_Appointments_Availability instance for the given availability ID.
 * Returns false if the availability rule cannot be loaded or an error occurs.
 *
 * @since 1.0.0
 *
 * @param int|string $id Optional. Availability rule ID. Default empty string.
 *
 * @return WC_Appointments_Availability|false Availability object on success, false on failure.
 *
 * @example
 * // Get availability rule
 * $availability = get_wc_appointments_availability( 789 );
 * if ( $availability ) {
 *     echo $availability->get_title();
 * }
 */
function get_wc_appointments_availability( $id = '' ) { // phpcs:ignore Generic.CodeAnalysis.UselessOverridingMethod.Found
	try {
		return new WC_Appointments_Availability( $id );
	} catch ( Exception $e ) {
		return false;
	}
}

/**
 * Sanitize and format a string into a valid 24-hour time format.
 *
 * Cleans the input string and converts it to 'H:i' format (24-hour time).
 * Handles various time input formats and normalizes them to HH:MM.
 *
 * @since 1.0.0
 *
 * @param string $raw_time Raw time string to sanitize and format.
 *
 * @return string Formatted time in 'H:i' format (e.g., '14:30').
 *
 * @example
 * // Sanitize various time formats
 * $time1 = wc_appointment_sanitize_time( '2:30 PM' );  // Returns '14:30'
 * $time2 = wc_appointment_sanitize_time( '14:30:00' ); // Returns '14:30'
 * $time3 = wc_appointment_sanitize_time( '2pm' );      // Returns '14:00'
 */
function wc_appointment_sanitize_time( $raw_time ): string {
	$time = wc_clean( $raw_time );

	return date( 'H:i', strtotime( $time ) );
}

/**
 * Check if a product is an appointment product.
 *
 * Verifies that the given product is a valid WC_Product_Appointment instance.
 *
 * @since 1.0.0
 *
 * @param mixed $product Product object, ID, or other value to check.
 *
 * @return bool True if product is a WC_Product_Appointment instance, false otherwise.
 *
 * @example
 * // Check if product is appointable
 * $product = wc_get_product( 123 );
 * if ( is_wc_appointment_product( $product ) ) {
 *     // Handle appointment product
 * }
 */
function is_wc_appointment_product( $product ): bool {
	return isset( $product ) && is_object( $product ) && is_a( $product, 'WC_Product_Appointment' );
}

/**
 * Convert appointment data key to a human-readable label.
 *
 * Returns a translated label for common appointment data keys (staff, date, time, duration).
 * Labels can be customized via the 'woocommerce_appointments_data_labels' filter.
 *
 * @since 1.0.0
 *
 * @param string                $key     Data key to get label for (e.g., 'staff', 'date', 'time', 'duration').
 * @param WC_Product_Appointment $product Product instance (used for staff label customization).
 *
 * @return string Human-readable label for the key, or the key itself if no label exists.
 *
 * @example
 * // Get label for staff field
 * $label = get_wc_appointment_data_label( 'staff', $product );
 * // Returns: 'Providers' (or custom staff label from product)
 */
function get_wc_appointment_data_label( string $key, $product ): string {
	$labels = apply_filters(
	    'woocommerce_appointments_data_labels',
	    [
			'staff'    => ( $product->get_staff_label() ?: __( 'Providers', 'woocommerce-appointments' ) ),
			'date'     => __( 'Date', 'woocommerce-appointments' ),
			'time'     => __( 'Time', 'woocommerce-appointments' ),
			'duration' => __( 'Duration', 'woocommerce-appointments' ),
		],
	);

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

	return $labels[ $key ];
}

/**
 * Convert appointment status to human-readable label.
 *
 * Returns a translated label for the given appointment status. Statuses include:
 * unpaid, pending-confirmation, confirmed, paid, cancelled, complete, in-cart.
 * Labels can be customized via the 'woocommerce_appointments_get_status_label' filter.
 *
 * @since 3.0.0
 *
 * @param string $status Appointment status key.
 *
 * @return string Human-readable status label, or the status key if no label exists.
 *
 * @example
 * // Get status label
 * $label = wc_appointments_get_status_label( 'pending-confirmation' );
 * // Returns: 'Pending Confirmation'
 */
function wc_appointments_get_status_label( string $status ): string {
	$statuses = [
		WC_Appointments_Constants::STATUS_UNPAID               => __( 'Unpaid', 'woocommerce-appointments' ),
		WC_Appointments_Constants::STATUS_PENDING_CONFIRMATION => __( 'Pending Confirmation', 'woocommerce-appointments' ),
		WC_Appointments_Constants::STATUS_CONFIRMED            => __( 'Confirmed', 'woocommerce-appointments' ),
		WC_Appointments_Constants::STATUS_PAID                 => __( 'Paid', 'woocommerce-appointments' ),
		WC_Appointments_Constants::STATUS_CANCELLED            => __( 'Cancelled', 'woocommerce-appointments' ),
		WC_Appointments_Constants::STATUS_COMPLETE             => __( 'Complete', 'woocommerce-appointments' ),
		WC_Appointments_Constants::STATUS_IN_CART              => __( 'In Cart', 'woocommerce-appointments' ),
	];

	/**
	 * Filter the return value of wc_appointments_get_status_label.
	 *
	 * @since 3.5.6
	 */
	$statuses = apply_filters( 'woocommerce_appointments_get_status_label', $statuses );

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

/**
 * Get a list of appointment statuses.
 *
 * Returns appointment statuses based on the provided context. Statuses can be
 * returned as keys only or as key-value pairs with translated labels.
 *
 * @since 2.3.0
 * @since 2.3.0 Added $include_translation_strings parameter.
 *
 * @param string|null $context                  Optional. Context filter ('user', 'validate', 'customer', 'cancel', 'scheduled', 'all', 'fully_scheduled'). Default 'fully_scheduled'.
 * @param bool        $include_translation_strings Optional. Whether to include translated labels. Default false.
 *
 * @return array<string>|array<string, string> {
 *     Return format depends on $include_translation_strings:
 *     - If false: array<int, string> Array of status keys (e.g., ['unpaid', 'confirmed', 'paid']).
 *     - If true:  array<string, string> Associative array with status keys as keys and translated labels as values
 *                (e.g., ['unpaid' => 'Unpaid', 'confirmed' => 'Confirmed']).
 * }
 *
 * @example
 * // Get status keys only
 * $statuses = get_wc_appointment_statuses( 'user' );
 * // Returns: ['unpaid', 'pending-confirmation', 'confirmed', ...]
 *
 * // Get statuses with labels
 * $statuses = get_wc_appointment_statuses( 'user', true );
 * // Returns: ['unpaid' => 'Unpaid', 'confirmed' => 'Confirmed', ...]
 */
function get_wc_appointment_statuses( ?string $context = 'fully_scheduled', bool $include_translation_strings = false ): array {
	// Handle null context by using default.
	if ( null === $context ) {
		$context = 'fully_scheduled';
	}
	if ( 'user' === $context ) {
		$statuses = apply_filters(
		    'woocommerce_appointment_statuses_for_user',
		    [
				WC_Appointments_Constants::STATUS_UNPAID               => __( 'Unpaid', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_PENDING_CONFIRMATION => __( 'Pending Confirmation', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_CONFIRMED            => __( 'Confirmed', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_PAID                 => __( 'Paid', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_CANCELLED            => __( 'Cancelled', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_COMPLETE             => __( 'Complete', 'woocommerce-appointments' ),
			],
		);
    } elseif ( 'validate' === $context ) {
        $statuses = apply_filters(
            'woocommerce_appointment_statuses_for_validation',
            [
                WC_Appointments_Constants::STATUS_UNPAID               => __( 'Unpaid', 'woocommerce-appointments' ),
                WC_Appointments_Constants::STATUS_PENDING_CONFIRMATION => __( 'Pending Confirmation', 'woocommerce-appointments' ),
                WC_Appointments_Constants::STATUS_CONFIRMED            => __( 'Confirmed', 'woocommerce-appointments' ),
                WC_Appointments_Constants::STATUS_PAID                 => __( 'Paid', 'woocommerce-appointments' ),
                WC_Appointments_Constants::STATUS_CANCELLED            => __( 'Cancelled', 'woocommerce-appointments' ),
            ],
        );
	} elseif ( 'customer' === $context ) {
		$statuses = apply_filters(
		    'woocommerce_appointment_statuses_for_customer',
		    [
				'expected' => __( 'Expected', 'woocommerce-appointments' ),
				'arrived'  => __( 'Arrived', 'woocommerce-appointments' ),
				'no-show'  => __( 'No-show', 'woocommerce-appointments' ),
			],
		);
	} elseif ( 'cancel' === $context ) {
		$statuses = apply_filters(
		    'woocommerce_appointment_statuses_for_cancel',
		    [
				WC_Appointments_Constants::STATUS_UNPAID               => __( 'Unpaid', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_PENDING_CONFIRMATION => __( 'Pending Confirmation', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_CONFIRMED            => __( 'Confirmed', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_PAID                 => __( 'Paid', 'woocommerce-appointments' ),
			],
		);
	} elseif ( 'scheduled' === $context ) {
		$statuses = apply_filters(
		    'woocommerce_appointment_statuses_for_scheduled',
		    [
				WC_Appointments_Constants::STATUS_CONFIRMED => __( 'Confirmed', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_PAID      => __( 'Paid', 'woocommerce-appointments' ),
			],
		);
	} elseif ( 'all' === $context ) {
		$statuses = apply_filters(
		    'woocommerce_appointment_statuses_for_all',
		    [
				WC_Appointments_Constants::STATUS_UNPAID               => __( 'Unpaid', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_PAID                 => __( 'Paid', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_PENDING_CONFIRMATION => __( 'Pending Confirmation', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_CONFIRMED            => __( 'Confirmed', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_CANCELLED            => __( 'Cancelled', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_COMPLETE             => __( 'Complete', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_WAS_IN_CART          => __( 'Was In Cart', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_IN_CART              => __( 'In Cart', 'woocommerce-appointments' ),
			],
		);
	} else {
		$statuses = apply_filters(
		    'woocommerce_appointment_statuses_for_fully_scheduled',
		    [
				WC_Appointments_Constants::STATUS_UNPAID               => __( 'Unpaid', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_PAID                 => __( 'Paid', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_PENDING_CONFIRMATION => __( 'Pending Confirmation', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_CONFIRMED            => __( 'Confirmed', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_COMPLETE             => __( 'Complete', 'woocommerce-appointments' ),
				WC_Appointments_Constants::STATUS_IN_CART              => __( 'In Cart', 'woocommerce-appointments' ),
			],
		);
	}

	/**
 	 * Filter the return value of get_wc_appointment_statuses.
 	 *
 	 * @since 3.5.6
 	 */
	$statuses = apply_filters( 'woocommerce_appointments_get_wc_appointment_statuses', $statuses );

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

/**
 * Acquire a booking lock for a specific time slot to prevent race conditions.
 *
 * Creates a transient-based lock to prevent concurrent bookings of the same time slot.
 * Locks are automatically released after a short TTL (default 5 seconds).
 *
 * @since 4.0.0
 *
 * @param int        $product_id Product ID.
 * @param int        $start_date Start timestamp.
 * @param int        $end_date   End timestamp.
 * @param int|int[]  $staff_id   Optional. Staff ID(s). Default 0.
 *
 * @return bool True if lock acquired, false if already locked.
 */
function wc_appointments_acquire_booking_lock( int $product_id, int $start_date, int $end_date, $staff_id = 0 ): bool {
	// Use booking lock class for consistency.
	$lock = new WC_Appointment_Booking_Lock();
	return $lock->acquire( $product_id, $start_date, $end_date, $staff_id );
}

/**
 * Release a booking lock.
 *
 * Removes the transient-based lock for a specific time slot.
 *
 * @since 4.0.0
 *
 * @param int       $product_id Product ID.
 * @param int       $start_date Start timestamp.
 * @param int       $end_date   End timestamp.
 * @param int|int[] $staff_id   Optional. Staff ID(s). Default 0.
 */
function wc_appointments_release_booking_lock( int $product_id, int $start_date, int $end_date, $staff_id = 0 ): void {
	// Use booking lock class for consistency.
	$lock = new WC_Appointment_Booking_Lock();
	$lock->release( $product_id, $start_date, $end_date, $staff_id );
}

/**
 * Validate and create a new appointment manually.
 *
 * Creates a new appointment for the specified product with the provided data.
 * Validates availability and optionally finds the next available slot if the requested
 * time is unavailable (when $exact is false).
 *
 * @since 1.10.7
 *
 * @see WC_Appointment::new_appointment() for available $new_appointment_data args
 *
 * @param int                  $product_id            Product ID for the appointment.
 * @param array<string, mixed> $new_appointment_data  Optional. Appointment data. {
 *     @type string $start_date Start date/time (timestamp or date string).
 *     @type string $end_date   End date/time (timestamp or date string).
 *     @type int    $staff_id   Staff ID (single staff).
 *     @type array  $staff_ids  Staff IDs (multiple staff).
 *     @type string $timezone   Timezone string.
 * }
 * @param string               $status                Optional. Initial appointment status. Default 'confirmed'.
 * @param bool                 $exact                 Optional. If false, find next available slot if requested time unavailable. Default false.
 *
 * @return WC_Appointment|false Appointment object on success, false on failure.
 *
 * @throws Exception If appointment cannot be created (logged, not thrown).
 *
 * @example
 * // Create appointment for tomorrow at 2 PM
 * $appointment = create_wc_appointment(
 *     123,
 *     [
 *         'start_date' => strtotime( 'tomorrow 14:00' ),
 *         'end_date'   => strtotime( 'tomorrow 15:00' ),
 *         'staff_id'   => 5,
 *     ],
 *     'confirmed',
 *     true  // Exact time required
 * );
 */
function create_wc_appointment( int $product_id, $new_appointment_data = [], $status = WC_Appointments_Constants::STATUS_CONFIRMED, $exact = false ) {
	// Use booking handler for appointment creation.
	$handler = new WC_Appointment_Booking_Handler();
	$result  = $handler->create_appointment( $product_id, $new_appointment_data, $status, $exact );

	// Maintain backward compatibility: return false on error (not WP_Error).
	if ( is_wp_error( $result ) ) {
		return false;
	}

	return $result;
}

/**
 * Check if product/appointment requires confirmation.
 *
 * @since 1.0.0
 *
 * @param int $id Product ID.
 *
 * @return bool True if product requires confirmation, false otherwise.
 */
function wc_appointment_requires_confirmation( int $id ): bool {
	$product = wc_get_product( $id );
    return is_wc_appointment_product( $product ) && $product->requires_confirmation();
}

/**
 * Check if product/appointment is sold individually.
 *
 * @since 1.0.0
 *
 * @param int $id Product ID.
 *
 * @return bool True if product is sold individually, false otherwise.
 */
function wc_appointment_sold_individually( int $id ): bool {
	$product = wc_get_product( $id );
    return is_wc_appointment_product( $product ) && $product->is_sold_individually();
}

/**
 * Check if the cart has appointment that requires confirmation.
 *
 * Iterates through cart contents to determine if any appointment product
 * requires confirmation before booking.
 *
 * @since 1.0.0
 *
 * @return bool True if any cart item requires confirmation, false otherwise.
 */
function wc_appointment_cart_requires_confirmation(): bool {
	$has_it = false;

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

	return $has_it;
}

/**
 * Check if the order has appointment that requires confirmation.
 *
 * Iterates through order items to determine if any appointment product
 * requires confirmation before booking.
 *
 * @since 1.0.0
 *
 * @param WC_Order|int|false $order Order object, order ID, or false.
 *
 * @return bool True if any order item requires confirmation, false otherwise.
 */
function wc_appointment_order_requires_confirmation( $order ): bool {
	$requires = false;

	if ( $order && is_a( $order, 'WC_Order' ) ) {
		foreach ( $order->get_items() as $item ) {
			if ( wc_appointment_requires_confirmation( $item['product_id'] ) ) {
				$requires = true;
				break;
			}
		}
	}

	return $requires;
}

/**
 * Get timezone string.
 *
 * Deprecated wrapper for wc_timezone_string(). Use wc_timezone_string() directly.
 * Inspired by https://wordpress.org/plugins/event-organiser/
 *
 * @since 1.0.0
 * @deprecated 4.22.5 Use wc_timezone_string() instead.
 *
 * @return string Timezone string (e.g., 'America/New_York', 'UTC+5').
 */
function wc_appointment_get_timezone_string(): string {
	wc_deprecated_function( __FUNCTION__, '4.22.5', 'wc_timezone_string' );
	return wc_timezone_string();
}

/**
 * Calculate appointment duration in minutes.
 *
 * Calculates the duration between start and end timestamps, optionally
 * formatting the result based on the duration unit.
 *
 * @since 1.0.0
 *
 * @param int    $start         Start timestamp.
 * @param int    $end           End timestamp.
 * @param string $duration_unit Optional. Duration unit for formatting. Default 'minute'.
 * @param bool   $pretty        Optional. Whether to return formatted string. Default true.
 *
 * @return string|int Formatted duration string if $pretty is true, otherwise minutes as integer.
 */
function wc_appointment_duration_in_minutes( int $start, int $end, ?string $duration_unit = WC_Appointments_Constants::DURATION_MINUTE, bool $pretty = true ) {
	$duration_unit = is_string( $duration_unit ) && '' !== $duration_unit ? $duration_unit : WC_Appointments_Constants::DURATION_MINUTE;
	if ( ! in_array( $duration_unit, WC_Appointments_Constants::get_duration_units(), true ) ) {
		$duration_unit = WC_Appointments_Constants::DURATION_MINUTE;
	}

	if ( $start && $end ) {
		$start_time = round( $start / 60 ) * 60; #round to nearest minute
		$end_time   = round( $end / 60 ) * 60; #round to nearest minute
		$timeDiff   = abs( $end_time - $start_time ); #calculate difference
		$time       = intval( $timeDiff / 60 ); #force integer value

		// Return minutes.
		if ( $pretty ) {
			return WC_Appointment_Duration::format_minutes( $time, $duration_unit );
		}

		return $time;
	}

	return 0;
}

/**
 * Convert timestamp to formated time.
 *
 * @param  $timestamp  int
 * @param  $is_all_day bool
 *
 * @return string|false
 */
function wc_appointment_format_timestamp( $timestamp, bool $is_all_day = false ) {
	if ( $timestamp ) {
		$date_format = wc_appointments_date_format();
		$time_format = ', ' . wc_appointments_time_format();
		if ( $is_all_day ) {
			return date_i18n( $date_format, $timestamp );
		}
        return date_i18n( $date_format . $time_format, $timestamp );
	}
	return false;
}

/**
 * Convert time in minutes to hours and minutes.
 *
 * @since 1.0.0
 * @deprecated 5.1.0 Use WC_Appointment_Duration::format_addon_duration() instead.
 *
 * @param int $time Time in minutes (can be negative).
 *
 * @return string|false Formatted addon duration with prefix, or false if time is empty/zero.
 */
function wc_appointment_pretty_addon_duration( $time ) {
	wc_deprecated_function( __FUNCTION__, '5.1.0', 'WC_Appointment_Duration::format_addon_duration()' );

	global $product;

	// Get product object from cart item if available.
	if ( ( is_cart() || is_checkout() ) && isset( $cart_item ) && null !== $cart_item ) {
		$product = wc_get_product( $cart_item->get_id() );
	}

	return WC_Appointment_Duration::format_addon_duration( (int) $time, $product );
}

/**
 * Convert duration in minutes to pretty time display.
 *
 * Formats a duration in minutes as a human-readable string based on the duration unit.
 *
 * @since 1.0.0
 * @deprecated 5.1.0 Use WC_Appointment_Duration::format_minutes() instead.
 *
 * @param int    $time          Duration in minutes.
 * @param string $duration_unit Optional. Duration unit for formatting. Default 'minute'.
 *
 * @return string Formatted duration string (e.g., "2 hours", "30 minutes").
 */
function wc_appointment_pretty_timestamp( int $time, ?string $duration_unit = WC_Appointments_Constants::DURATION_MINUTE ): string {
	wc_deprecated_function( __FUNCTION__, '5.1.0', 'WC_Appointment_Duration::format_minutes()' );

	$duration_unit = is_string( $duration_unit ) && '' !== $duration_unit ? $duration_unit : WC_Appointments_Constants::DURATION_MINUTE;
	if ( ! in_array( $duration_unit, WC_Appointments_Constants::get_duration_units(), true ) ) {
		$duration_unit = WC_Appointments_Constants::DURATION_MINUTE;
	}

	return WC_Appointment_Duration::format_minutes( $time, $duration_unit );
}

/**
 * Convert duration in minutes to array of duration parameters.
 * Convert time in minutes to duration parameters (duration and duration_unit).
 *
 * Calculates the most appropriate duration unit (month, day, hour, or minute)
 * based on the total time in minutes, breaking down larger units when possible.
 *
 * @since 4.9.8
 * @deprecated 5.1.0 Use WC_Appointment_Duration::from_minutes() instead.
 *
 * @param int $time Time in minutes to convert.
 *
 * @return array{
 *     duration: int,
 *     duration_unit: string
 * } Duration parameters array with 'duration' (int) and 'duration_unit' (string) keys.
 *   Duration unit will be one of: 'minute', 'hour', 'day', 'month'.
 *
 * @example
 * // Convert 1440 minutes (1 day) to duration parameters
 * $params = wc_appointment_duration_parameters( 1440 );
 * // Returns: ['duration' => 1, 'duration_unit' => 'day']
 *
 * @see WC_Appointment_Duration For a structured result object version with additional methods.
 */
function wc_appointment_duration_parameters( int $time ): array {
	wc_deprecated_function( __FUNCTION__, '5.1.0', 'WC_Appointment_Duration::from_minutes()' );

	$result = WC_Appointment_Duration::from_minutes( $time );
	return apply_filters( 'wc_appointment_duration_parameters', $result->to_array(), $time );
}

/**
 * Convert duration in minutes to duration parameters result object.
 *
 * Calculates the most appropriate duration unit (month, day, hour, or minute)
 * based on the total time in minutes, breaking down larger units when possible.
 * Returns a structured result object for type safety.
 *
 * @since 5.1.0
 * @deprecated 5.1.0 Use WC_Appointment_Duration::from_minutes() instead.
 *
 * @param int $time Time in minutes to convert.
 *
 * @return WC_Appointment_Duration Result object with duration and duration_unit.
 *
 * @example
 * // Convert 1440 minutes (1 day) to duration parameters
 * $result = WC_Appointment_Duration::from_minutes( 1440 );
 * $duration = $result->get_duration(); // 1
 * $unit = $result->get_duration_unit(); // 'day'
 * $minutes = $result->to_minutes(); // 1440
 * $pretty = $result->to_pretty_string(); // "1 day"
 */
function wc_appointment_duration_parameters_result( int $time ): WC_Appointment_Duration {
	wc_deprecated_function( __FUNCTION__, '5.1.0', 'WC_Appointment_Duration::from_minutes()' );
	return WC_Appointment_Duration::from_minutes( $time );
}

/**
 * Get timezone offset in seconds.
 *
 * Calculates the timezone offset in seconds for the site's configured timezone.
 * Falls back to GMT offset if timezone string is not available.
 *
 * @since 3.1.8
 *
 * @return int Timezone offset in seconds (can be negative for timezones behind UTC).
 */
function wc_appointment_timezone_offset(): int {
	$timezone = wc_timezone_string();

	if ( $timezone ) {
		$timezone_object = new DateTimeZone( $timezone );
		return $timezone_object->getOffset( new DateTime( 'now' ) );
	}
    return floatval( get_option( 'gmt_offset', 0 ) ) * HOUR_IN_SECONDS;
}

/**
 * Get the offset in seconds between a timezone and UTC.
 *
 * Calculates the timezone offset in seconds for a given timezone string.
 * Returns false if timezone is invalid or empty.
 *
 * @since 1.0.0
 *
 * @param string $timezone Timezone string (e.g., 'America/New_York', 'UTC+5').
 *
 * @return int|false Timezone offset in seconds, or false on failure.
 */
function wc_appointment_get_timezone_offset( string $timezone ) {
	if ( ! $timezone ) {
		return false;
	}

	// Map UTC+- timezones to gmt_offsets and set timezone_string to empty.
	if ( ! empty( $timezone ) && preg_match( '/^UTC[+-]/', $timezone ) ) {
		$gmt_offset = $timezone;
		$gmt_offset = preg_replace( '/UTC\+?/', '', $gmt_offset );
		$gmt_offset *= HOUR_IN_SECONDS;
		$timezone   = '';
	}

	if ( $timezone ) {
		$utc_tz   = new DateTimeZone( $timezone );
		$utc_date = new DateTime( 'now', $utc_tz );

		$offset = $utc_tz->getOffset( $utc_date );

		return (int) $offset;

	}
    $offset = $gmt_offset;
    return (int) $offset;
}

/**
 * Get timezone name.
 *
 * Returns a localized timezone name for display purposes.
 * Handles UTC offset timezones and loads translations for timezone names.
 *
 * @since 4.0.0
 *
 * @param string $timezone Optional. Timezone string. Default empty (uses site timezone).
 *
 * @return string Localized timezone name for display.
 */
function wc_appointment_get_timezone_name( $timezone = '' ): string {
	static $mo_loaded = false, $locale_loaded = null;

	// Map UTC+- timezones to gmt_offsets and set timezone_string to empty.
	if ( ! empty( $timezone ) && preg_match( '/^UTC[+-]/', $timezone ) ) {
		return $timezone;
	}

	$locale = get_user_locale();

	// Load translations for continents and cities.
	$continents = [
		'Africa',
		'America',
		'Antarctica',
		'Arctic',
		'Asia',
		'Atlantic',
		'Australia',
		'Europe',
		'Indian',
		'Pacific',
	];

	// Load translations for continents and cities.
	if ( ! $mo_loaded || $locale !== $locale_loaded ) {
		$locale_loaded = $locale ?: get_locale();
		$mofile        = WP_LANG_DIR . '/continents-cities-' . $locale_loaded . '.mo';
		unload_textdomain( 'continents-cities' );
		load_textdomain( 'continents-cities', $mofile );
		$mo_loaded = true;
	}

	$zone_name = '';
	foreach ( timezone_identifiers_list() as $zone ) {
		$zone_full = $zone;
		$zone      = explode( '/', $zone );
		if ( ! in_array( $zone[0], $continents ) ) {
			continue;
		}

		// This determines what gets set and translated - we don't translate Etc/* strings here, they are done later
		$exists    = [
			0 => ( isset( $zone[0] ) && $zone[0] ),
			1 => ( isset( $zone[1] ) && $zone[1] ),
			2 => ( isset( $zone[2] ) && $zone[2] ),
		];
		$exists[3] = ( $exists[0] && 'Etc' !== $zone[0] );
		$exists[4] = ( $exists[1] && $exists[3] );
		$exists[5] = ( $exists[2] && $exists[3] );

		$zonen = [
			'continent'   => ( $exists[0] ? $zone[0] : '' ),
			'city'        => ( $exists[1] ? $zone[1] : '' ),
			'subcity'     => ( $exists[2] ? $zone[2] : '' ),
			't_continent' => ( $exists[3] ? translate( str_replace( '_', ' ', $zone[0] ), 'continents-cities' ) : '' ),
			't_city'      => ( $exists[4] ? translate( str_replace( '_', ' ', $zone[1] ), 'continents-cities' ) : '' ),
			't_subcity'   => ( $exists[5] ? translate( str_replace( '_', ' ', $zone[2] ), 'continents-cities' ) : '' ),
		];

		if ( $timezone && $timezone === $zone_full ) {
			$zone_name = $zonen['t_city'];
		}
	}

	$timezone_name = $zone_name;

	if ( $timezone && $timezone_name ) {
		return $timezone_name;
	}
    return '';
}

/**
 * Convert Unix timestamps to/from various locales.
 *
 * Converts a timestamp from one timezone/locale to another, applying timezone offsets
 * and formatting the result according to the specified format.
 *
 * ============================================================================
 * TIMEZONE ARCHITECTURE - CRITICAL FOR DEVELOPERS
 * ============================================================================
 *
 * This function handles timezone conversion and is DST-AWARE.
 *
 * WHEN TO USE THIS FUNCTION:
 * ✅ Converting for DISPLAY when customer timezone differs from site timezone
 * ✅ Converting customer INPUT from their timezone TO site timezone
 * ✅ Email display in customer's preferred timezone
 *
 * WHEN NOT TO USE THIS FUNCTION:
 * ❌ Extracting time components for admin display (use date() directly)
 * ❌ Comparing timestamps for availability (timestamps are already comparable)
 * ❌ Storing timestamps (they should be site timezone as UTC)
 *
 * DST HANDLING:
 * This function uses DateTimeZone::getOffset() which automatically accounts for
 * Daylight Saving Time. The offset is calculated FOR THE SPECIFIC TIMESTAMP,
 * not as a fixed value.
 *
 * See docs/TIMEZONE_ARCHITECTURE.md for full explanation.
 * See docs/plans/TIMEZONE_MANAGEMENT_PLAN.md for comprehensive DST handling.
 * ============================================================================
 *
 * @since 1.0.0
 *
 * @param string $from         Source timezone identifier ('site', 'user', or timezone string). Default empty (GMT).
 * @param string $to           Target timezone identifier ('site', 'user', or timezone string). Default empty (GMT).
 * @param int    $time         Unix timestamp to convert.
 * @param string $format       Optional. PHP date format string. Default 'U' (Unix timestamp).
 * @param string $user_timezone Optional. User timezone string (used when $from or $to is 'user'). Default empty.
 * @param bool   $reverse      Optional. Reverse the conversion direction. Default false.
 *
 * @return string|false Formatted date string, or false if timestamp is invalid.
 *
 * @example
 * // Convert timestamp from site timezone to user timezone
 * $formatted = wc_appointment_timezone_locale( 'site', 'user', time(), 'Y-m-d H:i:s', 'America/New_York' );
 */
function wc_appointment_timezone_locale( string $from = '', string $to = '', $time = '', string $format = 'U', string $user_timezone = '', bool $reverse = false ) {
	if ( ! is_numeric( $time ) || PHP_INT_MAX < $time || $time < ~PHP_INT_MAX ) {
		return false;
	}

	$from_tz_str = ( 'site' === $from ) ? wc_timezone_string() : ( ( 'user' === $from ) ? $user_timezone : 'GMT' );
	$to_tz_str   = ( 'site' === $to ) ? wc_timezone_string() : ( ( 'user' === $to ) ? $user_timezone : 'GMT' );

	$offset_for = function( $tz_str ) use ( $time ): int {
		if ( ! empty( $tz_str ) && preg_match( '/^UTC[+-]/', $tz_str ) ) {
			$gmt_offset = preg_replace( '/UTC\+?/', '', $tz_str );
			return (int) round( (float) $gmt_offset * HOUR_IN_SECONDS );
		}
		if ( $tz_str ) {
			try {
				$tz = new DateTimeZone( $tz_str );
				$dt = new DateTimeImmutable( '@' . (int) $time );
				return $tz->getOffset( $dt );
			} catch ( Exception $e ) {
			}
		}
		$offset_hours = (float) get_option( 'gmt_offset', 0 );
		return (int) round( $offset_hours * HOUR_IN_SECONDS );
	};

	$from_offset = $offset_for( $from_tz_str );
	$to_offset   = $offset_for( $to_tz_str );

	$gmt  = $time - $from_offset;
	$date = date( $format, $gmt + $to_offset );

	if ( $reverse ) {
		$gmt  = $time + $from_offset;
		$date = date( $format, $gmt - $to_offset );
	}

	return $date;
}

/**
 * Filter core WP function wp_timezone_choice().
 *
 * Wrapper around wp_timezone_choice() with filter support for customizing
 * timezone choice dropdown output.
 *
 * @since 4.25.0
 *
 * @param string $selected Optional. Selected timezone value. Default empty.
 * @param string $locale   Optional. Locale for translations. Default empty (uses user locale).
 *
 * @return string HTML select dropdown with timezone choices.
 */
function wc_appointments_wp_timezone_choice( $selected = '', $locale = '' ): string {
	// Set up locale.
	if ( '' == $locale ) {
		$locale = get_user_locale();
	}

	// Filter out the choice.
	$timezones = apply_filters(
	    'woocommerce_appointments_wp_timezone_choice',
	    wp_timezone_choice( $selected, $locale ),
	    $selected,
	    $locale,
	);

	return $timezones;
}

/**
 * Convert minutes offset to timestamp.
 *
 * Adds the specified number of minutes to a base date and returns the resulting timestamp.
 *
 * @since 3.0.0
 *
 * @param int $minute    Number of minutes to add.
 * @param int $check_date Base timestamp to add minutes to.
 *
 * @return int Timestamp after adding minutes.
 *
 * @example
 * // Add 30 minutes to current time
 * $future_time = wc_appointment_minute_to_time_stamp( 30, time() );
 */
function wc_appointment_minute_to_time_stamp( $minute, $check_date ): int {
	return strtotime( "+ $minute minutes", $check_date );
}

/**
 * Convert a timestamp into the minutes after 0:00 (midnight).
 *
 * Calculates how many minutes have passed since midnight for a given timestamp.
 * Useful for time-of-day calculations and scheduling.
 *
 * @since 3.0.0
 *
 * @param int $timestamp Unix timestamp.
 *
 * @return int Minutes after midnight (0-1439).
 *
 * @example
 * // Get minutes after midnight for 2:30 PM
 * $minutes = wc_appointment_time_stamp_to_minutes_after_midnight( strtotime( '14:30' ) );
 * // Returns: 870 (14 * 60 + 30)
 */
function wc_appointment_time_stamp_to_minutes_after_midnight( $timestamp ): int {
	$hour = absint( date( 'H', $timestamp ) );
	$min  = absint( date( 'i', $timestamp ) );

	return $min + ( $hour * 60 );
}

/**
 * Convert a timestamp into ISO8601 format.
 *
 * Converts a Unix timestamp to ISO8601 date-time format (YYYY-MM-DDTHH:MM:SS)
 * using the site's configured timezone.
 *
 * @since 4.2.5
 *
 * @param int $timestamp Unix timestamp to convert.
 *
 * @return string ISO8601 formatted date-time string (e.g., '2024-01-15T14:30:00').
 */
function get_wc_appointment_time_as_iso8601( $timestamp ): string {
	$timezone    = wc_timezone_string();
	$server_time = new DateTime( date( 'Y-m-d\TH:i:s', $timestamp ), new DateTimeZone( $timezone ) );

	return $server_time->format( 'Y-m-d\TH:i:s' );
}

/**
 * Get explanation text for appointment availability rules.
 *
 * Returns translated explanation of how availability rules work (rules lower
 * in the table override rules higher in the table).
 *
 * @since 1.9.13
 *
 * @return string Translated explanation text about availability rules.
 */
function get_wc_appointment_rules_explanation(): string {
	return __( 'Rules further down the table will override those at the top.', 'woocommerce-appointments' );
}

/**
 * Get explanation text for appointment rule priority system.
 *
 * Returns translated explanation of how priority numbers work in appointment availability rules.
 *
 * @since 1.9.13
 *
 * @return string Translated explanation text about priority rules.
 */
function get_wc_appointment_priority_explanation(): string {
	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 staff rules. By using priority numbers you can execute rules in different orders.', 'woocommerce-appointments' );
}

/**
 * Write to WooCommerce log files.
 *
 * Adds a log entry to the WooCommerce logger with the specified log ID and message.
 *
 * @since 1.0.0
 *
 * @param string $log_id  Log identifier/context (e.g., 'appointments', 'booking').
 * @param string $message Log message to write.
 *
 * @return void
 *
 * @example
 * // Log an appointment creation
 * wc_add_appointment_log( 'appointments', 'Appointment #123 created successfully' );
 */
function wc_add_appointment_log( $log_id, $message ): void {
	$log = wc_get_logger();
	$log->add( $log_id, $message );
}

/**
 * Normalize staff IDs to an array format.
 *
 * Ensures staff IDs are always in array format, converting single values to arrays.
 * If empty, returns an empty array.
 *
 * @since 5.0.2
 *
 * @param int|int[]|mixed $staff_ids Staff ID(s) to normalize. Can be a single ID, array of IDs, or empty.
 * @return int[] Array of staff IDs. Empty array if input is empty or invalid.
 *
 * @example
 * // Single ID
 * $ids = wc_appointments_normalize_staff_ids( 123 ); // Returns [123]
 *
 * // Already an array
 * $ids = wc_appointments_normalize_staff_ids( [123, 456] ); // Returns [123, 456]
 *
 * // Empty
 * $ids = wc_appointments_normalize_staff_ids( [] ); // Returns []
 */
function wc_appointments_normalize_staff_ids( $staff_ids ): array {
	if ( empty( $staff_ids ) ) {
		return [];
	}

	if ( is_array( $staff_ids ) ) {
		return array_filter( array_map( 'intval', $staff_ids ) );
	}

	return [ (int) $staff_ids ];
}

/**
 * Validate appointment cart item structure.
 *
 * Checks if a cart item contains valid appointment data structure.
 * Returns true if the cart item has appointment data in the expected format.
 *
 * @since 5.0.2
 *
 * @param array $cart_item Cart item array to validate.
 * @return bool True if cart item has valid appointment data, false otherwise.
 *
 * @example
 * // Validate cart item
 * if ( wc_appointments_validate_cart_item( $cart_item ) ) {
 *     $appointment_data = $cart_item['appointment'];
 *     // Process appointment...
 * }
 */
function wc_appointments_validate_cart_item( array $cart_item ): bool {
	return ! empty( $cart_item['appointment'] ) && is_array( $cart_item['appointment'] );
}

/**
 * Get staff members from staff IDs.
 *
 * Retrieves staff member objects, names, or HTML links based on staff IDs.
 * Can return an array of staff objects, a comma-separated string of names, or HTML links.
 *
 * @since 1.0.0
 *
 * @param array|int $ids       Staff ID(s) - can be single ID or array of IDs.
 * @param bool      $names     Optional. If true, returns comma-separated string of names. Default false.
 * @param bool      $with_link Optional. If true, returns HTML links. Default false.
 *
 * @return array<int, WC_Product_Appointment_Staff>|string {
 *     Return format depends on parameters:
 *     - If $names is true: comma-separated string of staff names.
 *     - If $with_link is true: array of HTML anchor tags.
 *     - Otherwise: array of WC_Product_Appointment_Staff objects.
 * }
 *
 * @example
 * // Get staff objects
 * $staff = wc_appointments_get_staff_from_ids( [ 1, 2, 3 ] );
 *
 * // Get staff names as string
 * $names = wc_appointments_get_staff_from_ids( [ 1, 2 ], true );
 * // Returns: "John Doe, Jane Smith"
 */
function wc_appointments_get_staff_from_ids( $ids = [], bool $names = false, bool $with_link = false ) {
	if ( ! is_array( $ids ) ) {
		$ids = [ $ids ];
	}

	$staff_members = [];

	foreach ( $ids as $id ) {
			$staff_member = new WC_Product_Appointment_Staff( $id );

			if ( $with_link ) {
				$staff_members[] = '<a href="' . get_edit_user_link( $staff_member->get_id() ) . '">' . $staff_member->get_display_name() . '</a>';
			} elseif ( $names ) {
				$staff_members[] = $staff_member->get_display_name();
			} else {
				$staff_members[] = $staff_member;
			}
		}

	if ( $names ) {
		if ( [] !== $staff_members ) {
			return implode( ', ', $staff_members );
		}
		// Return empty string when names requested but no staff members found
		return '';
	}

	return $staff_members;
}

/**
 * Get the minimum timestamp that is appointable based on settings.
 *
 * Calculates the earliest bookable timestamp for a given date. If the date is today,
 * the offset starts from NOW rather than midnight. Otherwise, offset starts from midnight.
 *
 * @since 1.0.0
 *
 * @param int    $date   Date timestamp to calculate minimum time for.
 * @param int    $offset Offset value (e.g., 2 for "2 hours").
 * @param string $unit   Offset unit (e.g., 'hours', 'days', 'minutes').
 *
 * @return int Minimum appointable timestamp for the given date.
 */
function wc_appointments_get_min_timestamp_for_day( $date, $offset, $unit ): int {
	$timestamp = $date;

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

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

	return $timestamp;
}

/**
 * Get total available appointments for a date range.
 *
 * Calculates how many appointment slots are available for the requested quantity
 * within the specified date range. Returns availability data for all staff members
 * if product has staff, or a single availability value if no staff.
 *
 * Replaces the WC_Product_Appointment::get_available_appointments method.
 *
 * @since 1.0.0
 *
 * @param WC_Product_Appointment|int $appointable_product Product object or product ID.
 * @param int                        $start_date          Start date timestamp.
 * @param int                        $end_date            End date timestamp.
 * @param int|int[]|null             $staff_id            Optional. Staff ID(s) to check. Default null.
 * @param int                        $qty                 Optional. Quantity requested. Default 1.
 *
 * @return array<int, int>|int|false {
 *     Return format depends on product configuration:
 *     - If product has staff and 'all' assignment: array<int, int> Associative array with staff_id => available_qty
 *     - If product has single staff or no staff: int Available quantity for the slot
 *     - If no slots available or invalid dates: false
 *     - If WP_Error occurs: WP_Error object (rare)
 * }
 *
 * @example
 * // Check availability for a specific date range
 * $available = wc_appointments_get_total_available_appointments_for_range(
 *     $product,
 *     strtotime( '2024-01-15 10:00' ),
 *     strtotime( '2024-01-15 11:00' ),
 *     null,
 *     1
 * );
 * // Returns: 5 (if 5 slots available) or false (if none available)
 */
function wc_appointments_get_total_available_appointments_for_range( $appointable_product, $start_date, $end_date, $staff_id = null, $qty = 1 ) {
	// Alter the end date to limit it to go up to one slot if the setting is enabled
	if ( $appointable_product->get_availability_span() ) {
		$end_date = strtotime( '+ ' . $appointable_product->get_duration() . ' ' . $appointable_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 staff if needed
	if ( $appointable_product->has_staff() && ! is_numeric( $staff_id ) && !$appointable_product->is_staff_assignment_type( 'all' ) && (is_array($staff_id) && 1 < count( $staff_id )) ) {
		return false;
	}

	$min_date   = $appointable_product->get_min_date_a();
	$max_date   = $appointable_product->get_max_date_a();
	$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' === $appointable_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;
	}

	// Prefer indexed cache within horizon when enabled.
	$use_indexed = false;
	if ( class_exists( 'WC_Appointments_Cache_Availability' ) && method_exists( 'WC_Appointments_Cache_Availability', 'is_index_enabled' ) ) {
		$index_toggle = WC_Appointments_Cache_Availability::is_index_enabled();
		$horizon_months = function_exists( 'wc_appointments_get_cache_horizon_months' ) ? wc_appointments_get_cache_horizon_months() : 3;
		$horizon_ts   = strtotime( '+' . $horizon_months . ' months UTC' );
		$use_indexed  = $index_toggle && ( $end_date <= $horizon_ts );
	}

	if ( $use_indexed && class_exists( 'WC_Appointments_Controller' ) ) {
		// (Indexed path with preserved WP_Error messages injected earlier in this file.)
		// Call the same function to avoid duplication.
		$available_appointments = \WC_Appointments_Controller::get_indexed_total_available_for_range( $appointable_product, (int) $start_date, (int) $end_date, $staff_id, (int) $qty );

		if ( $appointable_product->has_staff() && $appointable_product->is_staff_assignment_type( 'all' ) && is_array( $available_appointments ) ) {
			$staff_ids = $appointable_product->get_staff_ids();
			foreach ( $staff_ids as $sid ) {
				if ( ! isset( $available_appointments[ $sid ] ) ) {
					return false;
				}
			}
		}

		return $available_appointments;
	}

	// Get availability of each staff - no staff has been chosen yet.
	if ( $appointable_product->has_staff() && ! $staff_id ) {
		$available_appointments = $appointable_product->get_all_staff_availability( $start_date, $end_date, $qty );

		if ( $appointable_product->is_staff_assignment_type( 'all' ) && is_array( $available_appointments ) ) {
			$staff_ids = $appointable_product->get_staff_ids();
			foreach ( $staff_ids as $sid ) {
				if ( ! isset( $available_appointments[ $sid ] ) ) {
					return false;
				}
			}
		}

		return $available_appointments;
	}
    // If we are checking for appointments for a specific staff, or have none.
    $check_date = $start_date;
    if ( in_array( $appointable_product->get_duration_unit(), [ WC_Appointments_Constants::DURATION_MINUTE, WC_Appointments_Constants::DURATION_HOUR ] ) ) {
			$time_rules_ok = WC_Product_Appointment_Rule_Manager::check_availability_rules_against_time( $appointable_product, $start_date, $end_date, $staff_id );
			if ( ! $time_rules_ok ) {
				return false;
			}
		} else {
			while ( $check_date < $end_date ) {
				if ( ! WC_Product_Appointment_Rule_Manager::check_availability_rules_against_date( $appointable_product, $check_date, $staff_id ) ) {
					return false;
				}
				if ( $appointable_product->get_availability_span() ) {
					break; // Only need to check first day
				}
				$check_date = strtotime( '+1 day', $check_date );
			}
		}
    // Get slots availability
    return $appointable_product->get_slots_availability( $start_date, $end_date, $qty, $staff_id, [] );
}

/**
 * Summary of appointment data for admin and checkout.
 *
 * Outputs a formatted summary list of appointment details including product,
 * providers/staff, date, and duration. Used in admin and checkout displays.
 *
 * @since 3.3.0
 *
 * @param WC_Appointment $appointment Appointment object to display summary for.
 * @param bool           $is_admin   Optional. Whether this is being called in admin context. Default false.
 *
 * @return void Outputs HTML directly.
 */
function wc_appointments_get_summary_list( $appointment, $is_admin = false ): void {
	$product   = $appointment->get_product();
	$providers = $appointment->get_staff_members( true );
	$label     = $product && is_callable( [ $product, 'get_staff_label' ] ) && $product->get_staff_label() ? $product->get_staff_label() : __( 'Providers', 'woocommerce-appointments' );
	$date      = sprintf( '%1$s', $appointment->get_start_date() );
	$date      = apply_filters( 'wc_appointments_summary_list_date', $date, $appointment, $is_admin );
	$duration  = sprintf( '%1$s', $appointment->get_duration() );

	$template_args = apply_filters(
	    'wc_appointments_get_summary_list',
	    [
			'appointment' => $appointment,
			'product'     => $product,
			'providers'   => $providers,
			'label'       => $label,
			'date'        => $date,
			'duration'    => $duration,
			'is_admin'    => $is_admin,
			'is_rtl'      => is_rtl() ? 'right' : 'left',
		],
	);

	wc_get_template( 'order/appointment-summary-list.php', $template_args, '', WC_APPOINTMENTS_TEMPLATE_PATH );
}

/**
 * Converts a string (e.g. 'yes' or 'no') to a boolean.
 *
 * Converts various string representations of boolean values to actual boolean.
 * Uses WooCommerce's wc_string_to_bool() if available, otherwise falls back to custom logic.
 *
 * @since 1.0.0
 *
 * @param string|bool|int $string Value to convert. Accepts 'yes', 'no', 'true', 'false', '1', '0', or actual boolean/int.
 *
 * @return bool Boolean representation of the input.
 *
 * @example
 * // Convert string to bool
 * $result = wc_appointments_string_to_bool( 'yes' );  // Returns true
 * $result = wc_appointments_string_to_bool( 'no' );   // Returns false
 */
function wc_appointments_string_to_bool( $string ): bool {
	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 );
}

/**
 * Escape and format RRULE string for iCal compatibility.
 *
 * Processes RRULE strings to ensure proper formatting for iCal/calendar systems.
 * Handles UNTIL date adjustments for all-day vs timed events.
 *
 * @since 4.5.14
 *
 * @param string $rrule      RRULE string to process (may contain multiple lines).
 * @param bool   $is_all_day Optional. Whether this is an all-day event. Default false.
 *
 * @return string Processed and escaped RRULE string.
 */
function wc_appointments_esc_rrule( $rrule, $is_all_day = false ): string {
	// Handle multi-line RRULE strings (RRULE + EXDATE lines).
	$lines = array_filter( array_map( 'trim', explode( "\n", $rrule ) ) );
	$processed_lines = [];

	foreach ( $lines as $line ) {
		// Only process RRULE lines for UNTIL adjustments.
		if ( strpos( $line, 'RRULE:' ) === 0 ) {
			// UNTIL is missing time.
			$until_time_missing = false;
			foreach ( explode( ';', $line ) as $pair ) {
				$pair = explode( '=', $pair );
                if (! isset( $pair[1] )) {
                    continue;
                }
                if (isset( $pair[2] )) {
                    continue;
                }
				[$key, $value] = $pair;
				if (false === strpos( $value, 'T' )) {
                    $until_time_missing = true;
                    break;
                }
			}

			// Remove time from UNTIL.
			if ( $is_all_day ) {
				$line = preg_replace_callback(
				    '/UNTIL=([\dTZ]+)(?=;?)/',
				    function( array $matches ): string {
						$dtUntil = new WC_DateTime( substr( $matches[1], 0, 8 ) );
						return 'UNTIL=' . $dtUntil->format( 'Ymd' );
					},
				    $line,
				);

			// Append time to UNTIL.
			} elseif ( $until_time_missing ) {
				$line = preg_replace( '/UNTIL=[^;]*/', '\0T000000Z', $line );
			}
		}

		// Keep all lines (RRULE, EXDATE, etc.).
		$processed_lines[] = $line;
	}

	return implode( "\n", $processed_lines );
}

/**
 * Return appointment object.
 *
 * @since 4.6.0
 *
 * @param mixed $appointment Appointment object, appointment ID, or order object/ID.
 *
 * @return WC_Appointment|false Appointment object on success, false on failure.
 */
function wc_appointments_maybe_appointment_object( $appointment ) {
	// Check if provided $appointment_id is indeed an $appointment.
	if ( is_a( $appointment, 'WC_Appointment' ) ) {
		return $appointment;
	}
    // Check if provided $appointment_id is an $order.
    // Some extensions use only orders as email triggers
    // so make sure they are also included.
    $order = wc_get_order( $appointment );
    if ( is_a( $order, 'WC_Order' ) ) {
			// Get $appointment_ids from an $order.
			$appointment_ids = WC_Appointment_Data_Store::get_appointment_ids_from_order_id( $order->get_id() );
			if ( ! empty( $appointment_ids ) && isset( $appointment_ids[0] ) ) {
				// Set $appointment_id from an $order.
				$appointment_id = absint( $appointment_ids[0] );
				// Set $this->object from $appointment_id.
				return get_wc_appointment( $appointment_id );
			}
        return false;
		}
	return false;
}

/**
 * Get an array of formatted time values from a timestamp.
 *
 * Extracts various date/time components from a timestamp and returns them
 * in a structured array format for easy access.
 *
 * @since 1.0.0
 *
 * @param int $timestamp Unix timestamp to extract values from.
 *
 * @return array<string, int|string> {
 * @return array{
 *     timestamp: int,
 *     year: int,
 *     month: int,
 *     day: int,
 *     week: int,
 *     day_of_week: int,
 *     time: string
 * } Formatted time values array with all date/time components extracted from timestamp.
 */
function wc_appointments_get_formatted_times( $timestamp ): array {
	return [
		'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 ),
	];
}

/**
 * Get posted form data into a neat array.
 *
 * Processes and sanitizes appointment booking form data from $_POST or provided array.
 * Extracts date, time, staff, quantity, and other appointment-related fields.
 *
 * @since 4.7.0
 *
 * @param array<string, mixed> $posted   Optional. Posted data array. Default empty (uses $_POST).
 * @param WC_Product|false     $product  Optional. Product object. Default false.
 * @param string|bool          $get_data Optional. Additional data retrieval flag. Default false.
 *
 * @return array<string, mixed>|false {
 *     Processed appointment data array, or false on failure.
 *
 *     Common keys include:
 *     - _year (string): Year value
 *     - _month (string): Month value
 *     - _day (string): Day value
 *     - _date (string): Combined date string (Y-m-d)
 *     - _start_date (int): Start timestamp
 *     - _end_date (int): End timestamp
 *     - _staff_id (int|string): Staff ID (or empty string for 'all' assignment)
 *     - staff (string): Staff name(s)
 *     - _qty (int): Quantity
 *     - _timezone (string): Timezone string
 *     - _local_timezone (string): Local timezone string
 *     - Additional keys may be present based on product configuration
 * }
 */
/**
 * Get and process posted appointment data with timezone handling.
 *
 * TIMEZONE ARCHITECTURE:
 * ======================
 * This function processes appointment form data and handles timezone conversion when necessary.
 *
 * Core Principle:
 * - Timestamps are stored as UTC but represent times in the site's configured timezone
 * - When customer timezone differs from site timezone, conversion happens here
 * - Final timestamps always represent site timezone times (stored as UTC)
 *
 * Timezone Handling:
 * 1. If product supports timezones AND customer selected a timezone:
 *    - Parse input in customer's timezone
 *    - Convert to site timezone
 *    - Store as UTC timestamp (representing site timezone time)
 *    - Save customer timezone in _local_timezone for future reference
 *
 * 2. If no customer timezone or product doesn't support timezones:
 *    - Parse input in site timezone
 *    - Store as UTC timestamp (representing site timezone time)
 *
 * 3. Final timestamps (_start_date, _end_date):
 *    - Always UTC timestamps representing site timezone times
 *    - Use local methods (date(), format()) to extract components
 *    - Never use UTC-specific methods when extracting
 *
 * Future Customer Timezone Support:
 * - When customer accounts have timezone preferences, this function will:
 *   1. Get customer timezone from account settings
 *   2. Convert customer timezone → site timezone → UTC timestamp
 *   3. Store both timezone and local_timezone metadata
 *
 * @param array  $posted  Posted form data (defaults to $_POST if empty).
 * @param object $product Product object (WC_Product_Appointment).
 * @param string $get_data Optional. Specific data key to return.
 *
 * @return array|mixed Processed appointment data array, or specific value if $get_data provided.
 */
function wc_appointments_get_posted_data( $posted = [], $product = false, $get_data = false ) {
	if ( empty( $posted ) ) {
		$posted = $_POST;
	}

	$data = [
		'_year'           => '',
		'_month'          => '',
		'_day'            => '',
		'_timezone'       => '',        // Site timezone (always set)
		'_local_timezone' => '',        // Customer timezone (if different from site)
	];

	// Get year month field.
	if ( ! empty( $posted['wc_appointments_field_start_date_yearmonth'] ) ) {
		$yearmonth      = strtotime( $posted['wc_appointments_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 date fields (y, m, d).
	} elseif (
		! empty( $posted['wc_appointments_field_start_date_year'] ) &&
		! empty( $posted['wc_appointments_field_start_date_month'] ) &&
		! empty( $posted['wc_appointments_field_start_date_day'] )
	) {
		$data['_year']  = absint( $posted['wc_appointments_field_start_date_year'] );
		$data['_year']  = $data['_year'] ?: date( 'Y' );
		$data['_month'] = absint( $posted['wc_appointments_field_start_date_month'] );
		$data['_day']   = absint( $posted['wc_appointments_field_start_date_day'] );
		$data['_date']  = $data['_year'] . '-' . $data['_month'] . '-' . $data['_day'];
		$data['date']   = date_i18n( wc_appointments_date_format(), strtotime( $data['_date'] ) );
	}

	// Get time field.
	if ( ! empty( $posted['wc_appointments_field_start_date_time'] ) ) {
		$data['_time'] = wc_clean( $posted['wc_appointments_field_start_date_time'] );
		$data['time']  = date_i18n( wc_appointments_time_format(), strtotime( "{$data['_year']}-{$data['_month']}-{$data['_day']} {$data['_time']}" ) );
	} else {
		$data['_time'] = '';
	}

	// Quantity being scheduled.
	$data['_qty'] = absint( $posted['quantity'] ?? 1 );

	// TIMEZONE HANDLING:
	// ==================
	// Process timezone conversion if product supports timezones and customer selected one.
	// This is where customer timezone → site timezone conversion happens.
	//
	// Flow:
	// 1. Customer selects time in their timezone (if product supports it)
	// 2. Parse timestamp in customer timezone
	// 3. Convert to site timezone
	// 4. Store as UTC timestamp (representing site timezone time)
	// 5. Save customer timezone in _local_timezone for future reference
	//
	// Note: Final timestamps always represent site timezone times, stored as UTC.
	// When extracting components later, use local methods (date(), format()) not UTC methods.
	if ( $product->has_timezones() && isset( $posted['wc_appointments_field_timezone'] ) && $posted['wc_appointments_field_timezone'] && ! empty( $posted['wc_appointments_field_start_date_time'] ) ) {
		$site_tzstring = wc_timezone_string();
		$tzstring      = sanitize_text_field( wp_unslash( $_COOKIE['appointments_time_zone'] ?? '' ) ) ?: 'UTC';
		$tzstring      = $posted['wc_appointments_field_timezone'] ?? $tzstring;

		// Set customer timezone for saving (for future customer account timezone support).
		$data['_local_timezone'] = $tzstring;

		// Convert customer timezone → site timezone if different.
		if ( $tzstring !== $site_tzstring ) {
			// Parse input timestamp in customer timezone.
			$timestamp = strtotime( "{$data['_year']}-{$data['_month']}-{$data['_day']} {$data['_time']}" );
			// Convert to site timezone.
			$local_timestamp = wc_appointment_timezone_locale( 'site', 'user', $timestamp, 'U', $tzstring, true );

			// Extract components using local methods (timestamp now represents site timezone time).
			$data['_day']  = date( 'd', $local_timestamp );
			$data['_date'] = date( 'Y-m-d', $local_timestamp );
			$data['_time'] = date( 'H:i', $local_timestamp );
		}
	}

	// Set appointment timezone for saving (always site timezone).
	// This is the timezone that the stored timestamps represent.
	$data['_timezone'] = wc_timezone_string();

	// Fixed duration.
	$product_duration_unit = $product->get_duration_unit();
	$product_duration_unit = is_string( $product_duration_unit ) && '' !== $product_duration_unit ? $product_duration_unit : WC_Appointments_Constants::DURATION_MINUTE;

	$duration_unit     = in_array( $product_duration_unit, [ WC_Appointments_Constants::DURATION_MINUTE, WC_Appointments_Constants::DURATION_HOUR ], true ) ? WC_Appointments_Constants::DURATION_MINUTE : $product_duration_unit;
	$duration_in_mins  = WC_Appointments_Constants::DURATION_HOUR === $product_duration_unit ? $product->get_duration() * 60 : $product->get_duration();
	$duration_in_total = WC_Appointments_Constants::DURATION_MONTH === $product_duration_unit ? $product->get_duration() : $duration_in_mins;
	$duration_total    = apply_filters( 'appointment_form_posted_total_duration', $duration_in_total, $product, $posted );

	// Display hours and minutes in a readable form.
	if ( WC_Appointments_Constants::DURATION_MONTH === $product_duration_unit ) {
		/* translators: %s: months in singular or plural */
		$total_duration_n = sprintf( _n( '%s month', '%s months', $duration_total, 'woocommerce-appointments' ), $duration_total );
	} elseif ( WC_Appointments_Constants::DURATION_DAY === $product_duration_unit ) {
		/* translators: %s: days in singular or plural */
		$total_duration_n = sprintf( _n( '%s day', '%s days', $duration_total, 'woocommerce-appointments' ), $duration_total );
	} else {
		/* translators: %s: minutes */
		$total_duration_n = WC_Appointment_Duration::format_minutes( $duration_total, WC_Appointments_Constants::DURATION_MINUTE );
	}

	#error_log( var_export( 'total_duration_n: ' . $total_duration_n, true ) );

	// Calculate start and end timestamps.
	// CRITICAL: These timestamps are UTC but represent times in site timezone.
	// When extracting components later, use local methods (date(), format()) not UTC methods.
	// See TIMEZONE_ARCHITECTURE.md for detailed explanation.
	if ( ! empty( $data['_time'] ) ) {
		// Time-based appointment: parse date + time in site timezone, store as UTC timestamp.
		$data['_start_date'] = strtotime( "{$data['_year']}-{$data['_month']}-{$data['_day']} {$data['_time']}" );
		$data['_end_date']   = strtotime( "+{$duration_total} {$duration_unit}", $data['_start_date'] );
		$data['_all_day']    = 0;
		$data['_duration']   = $duration_total;
		$data['duration']    = $total_duration_n;
	} elseif ( WC_Appointments_Constants::DURATION_NIGHT === $product_duration_unit ) {
		// Night-based appointment: parse date in site timezone, store as UTC timestamp.
		$data['_start_date'] = strtotime( "{$data['_year']}-{$data['_month']}-{$data['_day']}" );
		$data['_end_date']   = strtotime( "+{$duration_total} day", $data['_start_date'] );
		$data['_all_day']    = 0;
	} else {
		// Day/month-based appointment: parse date in site timezone, store as UTC timestamp.
		$data['_start_date'] = strtotime( "{$data['_year']}-{$data['_month']}-{$data['_day']}" );
		$data['_end_date']   = strtotime( "+{$duration_total} {$duration_unit} - 1 second", $data['_start_date'] );
		$data['_all_day']    = 1;
		$data['_duration']   = $duration_total;
		$data['duration']    = $total_duration_n;
	}


	#error_log( var_export( $data, true ) );

	#$data['duration'] = wc_appointment_pretty_timestamp( $product->get_duration_in_minutes(), $duration_unit );

	#error_log( var_export( $data, true ) );

	// If requested, return data directly
	// before going through expensive resources.
	if ( $get_data ) {
		return $data[ $get_data ] ?? false;
	}

	// Get posted staff or assign one for the date range.
	if ( $product->has_staff() ) {
		if ( $product->is_staff_assignment_type( 'customer' ) && ! empty( $posted['wc_appointments_field_staff'] ) ) {
			$staff = $product->get_staff_member( absint( $posted['wc_appointments_field_staff'] ) );
			if ( $staff ) {
				$data['_staff_id'] = $staff->get_id();
				$data['staff']     = $staff->get_display_name();
			} else {
				$data['_staff_id'] = 0;
			}
		} elseif ( $product->is_staff_assignment_type( 'all' ) ) {
			$staff = $product->get_staff();
			if ( $staff ) {
				$data['_staff_id'] = '';
				$data['staff']     = '';
				foreach ( $staff as $staff_member ) {
					$data['_staff_ids'][]   = $staff_member->get_id();
					$data['_staff_names'][] = $staff_member->get_display_name();
				}
				$data['_staff_id'] = $data['_staff_ids'][0];
				$staff_names       = is_array( $data['_staff_names'] ) ? $data['_staff_names'] : (array) $data['_staff_names'];
				$data['staff']     = implode( ', ', $staff_names );
			} else {
				$data['_staff_id'] = 0;
			}
		} else {
			// Assign an available staff automatically
			$available_appointments = wc_appointments_get_total_available_appointments_for_range(
			    $product,
			    $data['_start_date'],
			    $data['_end_date'],
			    0,
			    $data['_qty'],
			);

			if ( is_array( $available_appointments ) ) {
				$shuffleKeys = array_keys( $available_appointments );
				shuffle( $shuffleKeys ); // randomize
				$staff             = get_user_by( 'id', current( $shuffleKeys ) );
				$data['_staff_id'] = current( $shuffleKeys );
				$data['staff']     = $staff->display_name;
			}
		}
	}

	#error_log( var_export( $data, true ) );

	return apply_filters( 'woocommerce_appointments_get_posted_data', $data, $product, $posted );
}

/**
 * Attempt to convert a date formatting string from PHP to Moment.js format.
 *
 * Converts PHP date format characters to their Moment.js equivalents.
 * Used for JavaScript date formatting compatibility.
 *
 * @since 1.0.0
 *
 * @param string $format PHP date format string (e.g., 'Y-m-d H:i').
 *
 * @return string Moment.js format string (e.g., 'YYYY-MM-DD HH:mm').
 *
 * @example
 * // Convert PHP format to Moment format
 * $moment_format = wc_appointments_convert_to_moment_format( 'Y-m-d' );
 * // Returns: 'YYYY-MM-DD'
 */
function wc_appointments_convert_to_moment_format( $format ): string {
	$replacements = [
		'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',
	];

	return strtr( $format, $replacements );
}

/**
 * Renders a JSON object with a paginated availability set.
 *
 * Paginates an availability array and returns records for the specified page
 * along with the total count of records.
 *
 * @since 4.5.0
 *
 * @param array<int|string, mixed> $availability     Full availability array to paginate.
 * @param int|false                $page            Page number (1-based), or false for all records.
 * @param int                      $records_per_page Number of records per page.
 *
 * @return array{
 *     records: array<int|string, mixed>,
 *     count: int
 * } Paginated result with 'records' (paginated array) and 'count' (total count) keys.
 */
function wc_appointments_paginated_availability( $availability, $page, $records_per_page ): array {
	if ( false === $page ) {
		$records = $availability;
	} else {
		$records = array_slice( $availability, ( $page - 1 ) * $records_per_page, $records_per_page );
	}

	return [
		'records' => $records,
		'count'   => count( $availability ),
	];
}

/**
 * Return WordPress date format, defaulting to a non-empty one if it is unset.
 *
 * Gets the date format from WooCommerce settings, with fallback to 'F j, Y'.
 * Filterable via 'woocommerce_appointments_date_format'.
 *
 * @since 1.0.0
 *
 * @return string Date format string (e.g., 'F j, Y' for "January 15, 2024").
 */
function wc_appointments_date_format(): string {
	return apply_filters( 'woocommerce_appointments_date_format', wc_date_format() ?: 'F j, Y' );
}

/**
 * Return WordPress time format, defaulting to a non-empty one if it is unset.
 *
 * Gets the time format from WooCommerce settings, with fallback to 'g:i a'.
 * Filterable via 'woocommerce_appointments_time_format'.
 *
 * @since 1.0.0
 *
 * @return string Time format string (e.g., 'g:i a' for "2:30 pm").
 */
function wc_appointments_time_format(): string {
	return apply_filters( 'woocommerce_appointments_time_format', wc_time_format() ?: 'g:i a' );
}

/**
 * Search appointments.
 *
 * Searches for appointments by term, removing "Appointment #" prefix if present.
 * Uses the appointment data store to perform the search.
 *
 * @since 1.0.0
 *
 * @param string $term Search term (appointment ID or other searchable field).
 *
 * @return array<int> Array of appointment IDs matching the search term.
 */
function wc_appointment_search( string $term ): array {
	$data_store = WC_Data_Store::load( 'appointment' );
	return $data_store->search_appointments( str_replace( 'Appointment #', '', wc_clean( $term ) ) );
}

/**
 * (via Action Scheduler) Find available and scheduled slots for specific resources and update cache.
 *
 * Scheduled function that updates appointment slot availability cache for a product.
 * Deletes existing transients and regenerates time slots for the specified date range.
 *
 * @since 4.18.1
 *
 * @param int $product_id      Product ID to update slots for.
 * @param int $min_date        Starting date timestamp for the set of slots.
 * @param int $max_date        Ending date timestamp for the set of slots.
 * @param int $timezone_offset Timezone offset in seconds.
 *
 * @return void
 */
add_action( 'wc-appointment-scheduled-update-availability', 'wc_appointments_get_time_slots_scheduled', 10, 8 );
function wc_appointments_get_time_slots_scheduled( $product_id, $min_date, $max_date, $timezone_offset ): void {

	// Add meta to mark the event is started.
	$transient_name = 'schedule_ts_' . md5( http_build_query( [ $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_Appointments_Cache::delete_appointment_slots_transient();

	// Re-call the function to generate the latest time slots.
	WC_Appointments_Controller::find_scheduled_day_slots( $product_id, $min_date, $max_date, 'Y-n-j', $timezone_offset, [] );
}

/**
 * Get global availability rules.
 *
 * Deprecated wrapper for WC_Appointments_Availability_Data_Store::get_global_availability().
 * Use the data store method directly.
 *
 * @since 1.0.0
 * @deprecated 4.7.0 Use WC_Appointments_Availability_Data_Store::get_global_availability() instead.
 *
 * @param bool $with_gcal Optional. Whether to include Google Calendar availability. Default true.
 *
 * @return array<string, mixed> Global availability rules array.
 */
function wc_appointments_get_global_availability( $with_gcal = true ): array {
	wc_deprecated_function( __METHOD__, '4.7.0', 'WC_Appointments_Availability_Data_Store::get_global_availability()' );
	return WC_Appointments_Availability_Data_Store::get_global_availability( $with_gcal );
}

/**
 * Get staff availability rules.
 *
 * Deprecated wrapper for WC_Appointments_Availability_Data_Store::get_staff_availability().
 * Use the data store method directly.
 *
 * @since 1.0.0
 * @deprecated 4.7.0 Use WC_Appointments_Availability_Data_Store::get_staff_availability() instead.
 *
 * @param array<int> $staff_ids Optional. Array of staff IDs. Default empty array.
 *
 * @return array<string, mixed> Staff availability rules array.
 */
function wc_appointments_get_staff_availability( $staff_ids = [] ): array {
	wc_deprecated_function( __METHOD__, '4.7.0', 'WC_Appointments_Availability_Data_Store::get_staff_availability()' );
	return WC_Appointments_Availability_Data_Store::get_staff_availability( $staff_ids );
}

/**
 * Find available and scheduled slots for specific staff (if any) and return them as array.
 *
 * Deprecated wrapper for WC_Product_Appointment::get_time_slots().
 * Use the product method directly.
 *
 * @since 1.0.0
 * @deprecated 1.15.0 Use WC_Product_Appointment::get_time_slots() instead.
 *
 * @param array<string, mixed> $args Arguments for getting time slots.
 *
 * @return array<string, mixed> Available time slots array.
 */
function wc_appointments_get_time_slots( array $args ): array {
	wc_deprecated_function( __FUNCTION__, '1.15.0', 'WC_Product_Appointment::get_time_slots()' );
	$appointable_product = $args['product'];
	return $appointable_product->get_time_slots( $args );
}

/**
 * Find available slots and return HTML for the user to choose a slot.
 *
 * Deprecated wrapper for WC_Product_Appointment::get_time_slots_html().
 * Used in class-wc-appointments-ajax.php. Use the product method directly.
 *
 * @since 1.0.0
 * @deprecated 1.15.0 Use WC_Product_Appointment::get_time_slots_html() instead.
 *
 * @param array<string, mixed> $args Arguments for getting time slots HTML.
 *
 * @return string HTML markup for time slot selection.
 */
function wc_appointments_get_time_slots_html( array $args ): string {
	wc_deprecated_function( __FUNCTION__, '1.15.0', 'WC_Product_Appointment::get_time_slots_html()' );
	$appointable_product = $args['product'];
	return $appointable_product->get_time_slots_html( $args );
}

/**
 * Get the cache horizon in months for availability indexing.
 *
 * Retrieves the number of months ahead to cache appointment availability data.
 * Value is clamped between 1 and 12 months, with a default of 3 months.
 * Filterable for advanced customization.
 *
 * @since 4.15.0
 *
 * @return int Number of months to cache ahead (1-12, default 3).
 */
function wc_appointments_get_cache_horizon_months(): int {
	// Get the setting value, default to 3 months
	$months = get_option( 'wc_appointments_cache_horizon_months', 3 );

	// Ensure it's an integer and within valid range
	$months = absint( $months );
	$months = max( 1, min( 12, $months ) );

	/**
	 * Filter the cache horizon months for advanced customization.
	 *
	 * This allows advanced customers to extend the horizon beyond 12 months if needed.
	 *
	 * @param int $months Number of months to cache ahead
	 * @since 4.15.0
	 */
	return apply_filters( 'wc_appointments_cache_horizon_months', $months );
}
