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

/**
 * Class for the appointment product type.
 */
class WC_Product_Appointment extends WC_Product {

	/**
	 * Quantity basis constants.
	 */
	public const QTY_BASIS_SLOT = 'slot';
	public const QTY_BASIS_DAY = 'day';

	/**
	 * Duration unit constants.
	 */
	public const DURATION_UNIT_MINUTE = 'minute';
	public const DURATION_UNIT_HOUR = 'hour';
	public const DURATION_UNIT_DAY = 'day';
	public const DURATION_UNIT_MONTH = 'month';

	/**
	 * Stores product data.
	 *
	 * @var array
	 */
	protected array $defaults = [
		'has_price_label'         => false,
		'price_label'             => '',
		'has_pricing'             => false,
		'pricing'                 => [],
		'qty'                     => 1,
		'qty_basis'               => self::QTY_BASIS_SLOT,
		'qty_min'                 => 1,
		'qty_max'                 => 1,
		'duration_unit'           => self::DURATION_UNIT_HOUR,
		'duration'                => 1,
		'interval_unit'           => self::DURATION_UNIT_HOUR,
		'interval'                => 1,
		'padding_duration_unit'   => self::DURATION_UNIT_HOUR,
		'padding_duration'        => 0,
		'min_date_unit'           => self::DURATION_UNIT_DAY,
		'min_date'                => 0,
		'max_date_unit'           => self::DURATION_UNIT_MONTH,
		'max_date'                => 12,
		'user_can_cancel'         => false,
		'cancel_limit_unit'       => self::DURATION_UNIT_DAY,
		'cancel_limit'            => 1,
		'user_can_reschedule'     => false,
		'reschedule_limit_unit'   => self::DURATION_UNIT_DAY,
		'reschedule_limit'        => 1,
		'requires_confirmation'   => false,
		'customer_timezones'      => false,
		'cal_color'               => '',
		'availability_span'       => '',
		'availability_autoselect' => false,
		'has_restricted_days'     => false,
		'restricted_days'         => [],
		/*'availability'            => [],*/
		'staff_label'             => '',
		'staff_assignment'        => '',
		'staff_nopref'            => false,
		'staff_id'                => [],
		'staff_ids'               => [],
		'staff_base_costs'        => [],
		'staff_qtys'              => [],
	];

	/**
	 * Stores availability rules once loaded.
	 *
	 * @var array
	 */
	public array $availability_rules = [];

	/**
	 * Stores staff ID if auto assigned.
	 *
	 * @var int|false
	 */
	public int|false $assigned_staff_id = false;

	/**
	 * Merges appointment product data into the parent object.
	 *
	 * @param int|WC_Product|object $product Product to init.
	 */
	public function __construct( $product = 0 ) {
		/**
		 * Override default attributes for Product Appointment.
		 *
		 * @since 4.14.4
		 *
		 * @param array $defaults Default values to init new Product.
		 * @param int|WC_Product|object $product Product to init.
		 *
		 * @see WC_Product_Appointment::defaults
		 */
		$defaults   = apply_filters( 'woocommerce_appointments_product_defaults', $this->defaults, $product );
		$this->data = array_merge( $this->data, $defaults );
		parent::__construct( $product );
	}

	/**
	 * Get the add to cart button text
	 *
	 * @return string
	 */
	public function add_to_cart_text() {
		return apply_filters( 'woocommerce_appointment_add_to_cart_text', __( 'Book', 'woocommerce-appointments' ), $this );
	}

	/**
	 * Get the add to cart button text for the single page
	 *
	 * @return string
	 */
	public function single_add_to_cart_text() {
		return $this->get_requires_confirmation() ? apply_filters( 'woocommerce_appointment_single_check_availability_text', __( 'Check Availability', 'woocommerce-appointments' ), $this ) : apply_filters( 'woocommerce_appointment_single_add_to_cart_text', __( 'Book Now', 'woocommerce-appointments' ), $this );
	}

	/**
	 * Return if appointment has label
	 * @return bool
	 */
	public function has_price_label(): string|bool {
		$has_price_label = false;

		// Products must exist of course
		if ( $this->get_has_price_label() ) {
			$price_label     = $this->get_price_label();
			$has_price_label = $price_label ?: __( 'Price Varies', 'woocommerce-appointments' );
		}

		return $has_price_label;
	}

	/**
	 * Get price HTML
	 *
	 * @param string $price
	 * @return string
	 */
	public function get_price_html( $deprecated = '' ) {
		$sale_price    = wc_format_sale_price(
		    wc_get_price_to_display(
		        $this,
		        [
					'qty'   => 1,
					'price' => $this->get_regular_price(),
				],
		    ),
		    wc_get_price_to_display( $this ),
		) . $this->get_price_suffix();
		$regular_price = wc_price( floatval( wc_get_price_to_display( $this ) ) ) . $this->get_price_suffix();

		// Price.
		if ( '' === $this->get_price() ) {
			$price = apply_filters( 'woocommerce_empty_price_html', '<span class="amount">' . __( 'Free!', 'woocommerce-appointments' ) . '</span>', $this );
		} elseif ( $this->is_on_sale() ) {
			$price = $sale_price;
		} else {
			$price = $regular_price;
		}

		// Default price display.
		$price_html = $price;

		// Price with additional cost.
		if ( $this->has_additional_costs() ) {
			/* translators: %s: display price */
			$price_html = sprintf( __( 'From: %s', 'woocommerce-appointments' ), $price );
		}

		// Price label.
		if ( $this->has_price_label() ) {
			$price_html = $this->has_price_label();
		}

		// Duration HTML label.
		if ( self::DURATION_UNIT_MONTH === $this->get_duration_unit() ) {
			/* translators: %s: months in singular or plural */
			$duration_html = ' <small class="duration">' . sprintf( _n( '%s month', '%s months', $this->get_duration(), 'woocommerce-appointments' ), $this->get_duration() ) . '</small>';
		} elseif ( self::DURATION_UNIT_DAY === $this->get_duration_unit() ) {
			/* translators: %s: days in singular or plural */
			$duration_html = ' <small class="duration">' . sprintf( _n( '%s day', '%s days', $this->get_duration(), 'woocommerce-appointments' ), $this->get_duration() ) . '</small>';
		// Hourly or minutes product duration sets add-on duration in minutes.
		} else {
			$duration_full = WC_Appointment_Duration::format_minutes( $this->get_duration_in_minutes(), WC_Appointments_Constants::DURATION_MINUTE );
			$duration_html = ' <small class="duration">' . $duration_full . '</small>';
		}

		return apply_filters( 'woocommerce_get_price_html', apply_filters( 'woocommerce_return_price_html', $price_html, $this ) . apply_filters( 'woocommerce_return_duration_html', $duration_html, $this ), $this );
	}

	/**
	 * Get cost HTML
	 *
	 * @param string $cost
	 * @return string
	 */
	public function get_cost_html( float $cost = 0 ): string {
		if ( 'incl' === get_option( 'woocommerce_tax_display_shop' ) ) {
			$display_price = wc_get_price_including_tax(
			    $this,
			    [
					'price' => $cost,
				],
			);
		} else {
			$display_price = wc_get_price_excluding_tax(
			    $this,
			    [
					'price' => $cost,
				],
			);
		}

		/*
		$display_price_suffix  = wc_price( apply_filters( 'woocommerce_product_get_price', $display_price, $this ) ) . $this->get_price_suffix();
		$original_price_suffix = wc_price( $display_price ) . $this->get_price_suffix();

		if ( $original_price_suffix !== $display_price_suffix ) {
			$cost_html = "<del>{$original_price_suffix}</del><ins>{$display_price_suffix}</ins>";
		} elseif ( $display_price ) {
			$cost_html = wc_price( $display_price ) . $this->get_price_suffix();
		} else {
			$cost_html = __( 'Free!', 'woocommerce-appointments' );
		}
		*/

		// Formatted price display.
		$cost_html = wc_price( $display_price ) . $this->get_price_suffix( $cost, 1 );

		return apply_filters(
		    'woocommerce_get_cost_html',
		    $cost_html,
		    $this,
		);
	}

	/**
	 * Get internal type.
	 *
	 * @return string
	 */
	public function get_type() {
		return 'appointment';
	}

	/**
	 * @since 3.0.0
	 * @return bool
	 */
	public function is_wc_appointment_has_staff(): bool {
		return $this->has_staff();
	}

	/*
	|--------------------------------------------------------------------------
	| CRUD Getters and setters.
	|--------------------------------------------------------------------------
	*/

	/**
	 * Returns whether the product has additional costs.
	 *
	 * @param  string $context
	 * @return boolean
	 */
	public function get_has_additional_costs( string $context = 'view' ): bool {
		return (bool) $this->get_prop( 'has_additional_costs', $context );
	}

	/**
	 * Set has_additional_costs.
	 *
	 * @param boolean $value
	 */
	public function set_has_additional_costs( $value ): void {
		$this->set_prop( 'has_additional_costs', wc_appointments_string_to_bool( $value ) );
	}

	/**
	 * Returns whether the product has a custom price label.
	 *
	 * @param  string $context
	 * @return boolean
	 */
	public function get_has_price_label( string $context = 'view' ): bool {
		return (bool) $this->get_prop( 'has_price_label', $context );
	}

	/**
	 * Set has_price_label.
	 *
	 * @param boolean $value
	 */
	public function set_has_price_label( $value ): void {
		$this->set_prop( 'has_price_label', $value );
	}

	/**
	 * Returns the custom price label.
	 *
	 * @param  string $context
	 * @return string
	 */
	public function get_price_label( string $context = 'view' ): string {
		return (string) $this->get_prop( 'price_label', $context );
	}

	/**
	 * Set get_price_label.
	 *
	 * @param string $value
	 */
	public function set_price_label( $value ): void {
		$this->set_prop( 'price_label', $value );
	}

	/**
	 * Returns whether the product has custom pricing rules.
	 *
	 * @param  string $context
	 * @return boolean
	 */
	public function get_has_pricing( string $context = 'view' ): bool {
		return (bool) $this->get_prop( 'has_pricing', $context );
	}

	/**
	 * Set has_pricing.
	 *
	 * @param boolean $value
	 */
	public function set_has_pricing( $value ): void {
		$this->set_prop( 'has_pricing', $value );
	}

	/**
	 * Get pricing_rules.
	 *
	 * @param  string $context
	 * @return array
	 */
	public function get_pricing( string $context = 'view' ): array {
		return (array) $this->get_prop( 'pricing', $context );
	}

	/**
	 * Set pricing_rules.
	 *
	 * @param array $value
	 */
	public function set_pricing( $value ): void {
		$this->set_prop( 'pricing', (array) $value );
	}

	/**
	 * Returns the quantity available to schedule per slot.
	 *
	 * @param  string $context
	 * @return integer
	 */
	public function get_qty( string $context = 'view' ): int {
		return (int) $this->get_prop( 'qty', $context );
	}

	/**
	 * Set qty.
	 *
	 * @param integer $value
	 */
	public function set_qty( $value ): void {
		$this->set_prop( 'qty', absint( $value ) );
	}

	/**
	 * Get min qty available to schedule per slot.
	 *
	 * @param  string $context
	 * @return integer
	 */
	public function get_qty_min( string $context = 'view' ): int {
		return (int) $this->get_prop( 'qty_min', $context );
	}

	/**
	 * Set min qty.
	 *
	 * @param integer $value
	 */
	public function set_qty_min( $value ): void {
		$this->set_prop( 'qty_min', absint( $value ) );
	}
	/**
	 * Returns the maximum quantity allowed to schedule per slot.
	 *
	 * @param  string $context
	 * @return integer
	 */
	public function get_qty_max( string $context = 'view' ): int {
		return (int) $this->get_prop( 'qty_max', $context );
	}

	/**
	 * Set max qty.
	 *
	 * @param integer $value
	 */
	public function set_qty_max( $value ): void {
		$this->set_prop( 'qty_max', absint( $value ) );
	}

	/**
	 * Returns how quantity is applied (per slot or per day).
	 *
	 * @param string $context
	 * @return string
	 */
	public function get_qty_basis( string $context = 'view' ): string {
		$basis = $this->get_prop( 'qty_basis', $context );

		return in_array( $basis, [ self::QTY_BASIS_DAY, self::QTY_BASIS_SLOT ], true ) ? (string) $basis : self::QTY_BASIS_SLOT;
	}

	/**
	 * Set how quantity is applied (per slot or per day).
	 *
	 * @param string $value
	 */
	public function set_qty_basis( $value ): void {
		$basis = in_array( $value, [ self::QTY_BASIS_DAY, self::QTY_BASIS_SLOT ], true ) ? $value : self::QTY_BASIS_SLOT;
		$this->set_prop( 'qty_basis', $basis );
	}

	/**
	 * Helper to check if quantity is enforced per day.
	 *
	 * @return bool
	 */
	public function is_qty_per_day(): bool {
		return self::QTY_BASIS_DAY === $this->get_qty_basis();
	}

	/**
	 * Get effective max quantity for the frontend quantity input.
	 *
	 * Computes the highest configured maximum across:
	 * - Product per-appointment max (get_qty_max)
	 * - Availability rules capacities (global/product/staff rules)
	 * - Staff-defined quantities
	 *
	 * This is an upper bound for the quantity input; actual slot availability
	 * is validated later during cost calculation and add-to-cart.
	 *
	 * @since 4.18.x
	 * @return int
	 */
	public function get_qty_input_max(): int {
		static $memoized = [];

		$pid = (int) $this->get_id();
		if ( isset( $memoized[ $pid ] ) ) {
			return apply_filters( 'woocommerce_appointments_quantity_input_max', absint( $memoized[ $pid ] ), $this );
		}

		// Try transient cache keyed to appointments version for quick reuse.
		if ( class_exists( 'WC_Appointments_Cache' ) && class_exists( 'WC_Cache_Helper' ) ) {
			$version        = \WC_Cache_Helper::get_transient_version( 'appointments' );
			$transient_name = 'qty_input_max_' . $pid . '_' . md5( (string) $version );
			$cached_max     = \WC_Appointments_Cache::get( $transient_name );
			if ( false !== $cached_max && null !== $cached_max ) {
				$memoized[ $pid ] = (int) $cached_max;
				return apply_filters( 'woocommerce_appointments_quantity_input_max', absint( $memoized[ $pid ] ), $this );
			}
		}

		$max_candidates = [];

		// Product-defined maximum per appointment.
		$product_qty_max = absint( $this->get_qty_max() );
		if ( 0 < $product_qty_max ) {
			$max_candidates[] = $product_qty_max;
		}

		// Base product capacity per slot.
		$product_slot_capacity = absint( $this->get_qty() );
		if ( 0 < $product_slot_capacity ) {
			$max_candidates[] = $product_slot_capacity;
		}

		// Staff quantities: take the maximum available across staff.
		if ( $this->has_staff() ) {
			$staff_max_capacity = absint( $this->get_available_qty( '', false, true ) );
			if ( 0 < $staff_max_capacity ) {
				$max_candidates[] = $staff_max_capacity;
			}
		}

		// Fast-path: use indexed availability cache to compute the highest explicit rule qty.
		$rules_max_capacity = 0;
		$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();
			$use_indexed  = $index_toggle && class_exists( 'WC_Appointments_Availability_Cache_Data_Store' );
		}

		if ( $use_indexed ) {
			global $wpdb;
			$table = $wpdb->prefix . \WC_Appointments_Availability_Cache_Data_Store::TABLE_NAME;

			// Build staff list: product staff IDs plus general (0).
			$staff_ids = [];
			if ( $this->has_staff() ) {
				$staff_ids = array_map( 'intval', (array) $this->get_staff_ids() );
			}
			$staff_ids[] = 0;
			$staff_ids   = array_unique( array_filter( $staff_ids, static fn($v): bool => is_int( $v ) && 0 <= $v ) );

			$staff_in = [] !== $staff_ids ? implode( ',', array_map( 'intval', $staff_ids ) ) : '0';
			$product_id = $pid;

			// Compute MAX(qty) using UNION instead of OR for better performance.
			// UNION allows MySQL to use indexes more efficiently than OR conditions.
			$sql_parts = [];
			$union_params = [];
			
			// Global scope rules
			$sql_parts[] = "SELECT MAX(qty) AS qty FROM {$table}
				WHERE source = 'availability'
				  AND qty > 0
				  AND appointable = 'yes'
				  AND scope = 'global'
				GROUP BY source_id";
			
			// Product scope rules (always include if product_id is set)
			$sql_parts[] = "SELECT MAX(qty) AS qty FROM {$table}
				WHERE source = 'availability'
				  AND qty > 0
				  AND appointable = 'yes'
				  AND scope = 'product'
				  AND product_id = %d
				GROUP BY source_id";
			$union_params[] = $product_id;
			
			// Staff scope rules (staff_ids always contains at least 0)
			$staff_placeholders = implode( ',', array_fill( 0, count( $staff_ids ), '%d' ) );
			$sql_parts[] = "SELECT MAX(qty) AS qty FROM {$table}
				WHERE source = 'availability'
				  AND qty > 0
				  AND appointable = 'yes'
				  AND scope = 'staff'
				  AND staff_id IN ({$staff_placeholders})
				GROUP BY source_id";
			$union_params = array_merge( $union_params, $staff_ids );
			
			// Combine with UNION and get MAX
			$union_sql = '(' . implode( ') UNION (', $sql_parts ) . ')';
			$sql = "SELECT MAX(t.qty) FROM ({$union_sql}) AS t";
			
			$rules_max_capacity = (int) $wpdb->get_var( $wpdb->prepare( $sql, ...$union_params ) );
		}

		// Fallback: scan non-indexed rules if index not available or query returned zero.
		if ( 0 >= $rules_max_capacity ) {
			$rules = $this->get_availability_rules();
			if ( ! empty( $rules ) && is_array( $rules ) ) {
				foreach ( $rules as $rule ) {
					if ( isset( $rule['type'] ) && ':expired' === substr( $rule['type'], -8 ) ) {
						continue;
					}
					if ( isset( $rule['qty'] ) ) {
						$rules_max_capacity = max( $rules_max_capacity, absint( $rule['qty'] ) );
					}
				}
			}

			// Staff-specific availability rules.
			if ( $this->has_staff() ) {
				foreach ( (array) $this->get_staff_ids() as $staff_id ) {
					$srules = $this->get_availability_rules( $staff_id );
					if ( ! empty( $srules ) && is_array( $srules ) ) {
						foreach ( $srules as $rule ) {
							if ( isset( $rule['type'] ) && ':expired' === substr( $rule['type'], -8 ) ) {
								continue;
							}
							if ( isset( $rule['qty'] ) ) {
								$rules_max_capacity = max( $rules_max_capacity, absint( $rule['qty'] ) );
							}
						}
					}
				}
			}
		}

		if ( 0 < $rules_max_capacity ) {
			$max_candidates[] = $rules_max_capacity;
		}

		$computed_max = [] === $max_candidates ? 1 : max( $max_candidates );

		// Persist in request-level memo and transient cache for short-term reuse.
		$memoized[ $pid ] = (int) $computed_max;

		// Lightweight transient keyed to appointments cache version to auto-invalidate on changes.
		if ( class_exists( 'WC_Appointments_Cache' ) && class_exists( 'WC_Cache_Helper' ) ) {
			$version = \WC_Cache_Helper::get_transient_version( 'appointments' );
			$transient_name = 'qty_input_max_' . $pid . '_' . md5( (string) $version );
			\WC_Appointments_Cache::set( $transient_name, (int) $computed_max, DAY_IN_SECONDS * 3 );
		}

		return (int) apply_filters( 'woocommerce_appointments_quantity_input_max', absint( $computed_max ), $this );
	}

	/**
	 * Returns the duration unit (minute, hour, day, month).
	 *
	 * @param  string $context
	 * @return string
	 */
	public function get_duration_unit( string $context = 'view' ): string {
		$value = $this->get_prop( 'duration_unit', $context );

		if ('view' === $context) {
            return (string) apply_filters( 'woocommerce_appointments_get_duration_unit', $value, $this );
        }
		return (string) $value;
	}

	/**
	 * Set duration_unit.
	 *
	 * @param string $value
	 */
	public function set_duration_unit( $value ): void {
		$this->set_prop( 'duration_unit', (string) $value );
	}

	/**
	 * Returns the duration of the appointment.
	 *
	 * @param  string $context
	 * @return integer
	 */
	public function get_duration( string $context = 'view' ): int {
		$value = $this->get_prop( 'duration', $context );

		if ('view' === $context) {
            return (int) apply_filters( 'woocommerce_appointments_get_duration', $value, $this );
        }

		return (int) $value;
	}

	/**
	 * Get duration.
	 *
	 * @param  string $context
	 * @return integer
	 */
	public function get_duration_in_minutes(): int {
		$duration = self::DURATION_UNIT_HOUR === $this->get_duration_unit() ? $this->get_duration() * 60 : $this->get_duration();
		$duration = self::DURATION_UNIT_DAY === $this->get_duration_unit() ? $this->get_duration() * 60 * 24 : $duration;
		$duration = self::DURATION_UNIT_MONTH === $this->get_duration_unit() ? $this->get_duration() : $duration;

		return (int) apply_filters( 'woocommerce_appointments_get_duration_in_minutes', $duration, $this );
	}

	/**
	 * Set duration.
	 *
	 * @param integer $value
	 */
	public function set_duration( $value ): void {
		$this->set_prop( 'duration', absint( $value ) );
	}

	/**
	 * Returns the interval unit (minute, hour, day, month).
	 *
	 * @param  string $context
	 * @return string
	 */
	public function get_interval_unit( string $context = 'view' ): string {
		$value = $this->get_prop( 'interval_unit', $context );

		if ('view' === $context) {
            return (string) apply_filters( 'woocommerce_appointments_get_interval_unit', $value, $this );
        }
		return (string) $value;
	}

	/**
	 * Set interval_unit.
	 *
	 * @param string $value
	 */
	public function set_interval_unit( $value ): void {
		$this->set_prop( 'interval_unit', (string) $value );
	}

	/**
	 * Returns the interval between appointments.
	 *
	 * @param  string $context
	 * @return integer
	 */
	public function get_interval( string $context = 'view' ): int {
		return (int) $this->get_prop( 'interval', $context );
	}

	/**
	 * Set interval.
	 *
	 * @param integer $value
	 */
	public function set_interval( $value ): void {
		$this->set_prop( 'interval', absint( $value ) );
	}

	/**
	 * Get padding_duration_unit.
	 *
	 * @param  string $context
	 * @return string
	 */
	public function get_padding_duration_unit( string $context = 'view' ): string {
		$value = $this->get_prop( 'padding_duration_unit', $context );

		if ('view' === $context) {
            return (string) apply_filters( 'woocommerce_appointments_get_padding_duration_unit', $value, $this );
        }

		return (string) $value;
	}

	/**
	 * Set padding_duration_unit.
	 *
	 * @param string $value
	 */
	public function set_padding_duration_unit( $value ): void {
		$this->set_prop( 'padding_duration_unit', (string) $value );
	}

	/**
	 * Get padding_duration.
	 *
	 * @param  string $context
	 * @return integer
	 */
	public function get_padding_duration( string $context = 'view' ): int {
		$value = $this->get_prop( 'padding_duration', $context );

		if ('view' === $context) {
            return (int) apply_filters( 'woocommerce_appointments_get_padding_duration', $value, $this );
        }

		return (int) $value;
	}

	/**
	 * Get duration.
	 *
	 * @param  string $context
	 * @return integer
	 */
	public function get_padding_duration_in_minutes(): int {
		$duration = self::DURATION_UNIT_HOUR === $this->get_padding_duration_unit() ? $this->get_padding_duration() * 60 : $this->get_padding_duration();
		$duration = self::DURATION_UNIT_DAY === $this->get_padding_duration_unit() ? $this->get_padding_duration() * 60 * 24 : $duration;
		$duration = self::DURATION_UNIT_MONTH === $this->get_padding_duration_unit() ? $this->get_padding_duration() : $duration;

		return (int) apply_filters( 'woocommerce_appointments_get_padding_duration_in_minutes', $duration, $this );
	}

	/**
	 * Set padding_duration.
	 *
	 * @param integer $value
	 */
	public function set_padding_duration( $value ): void {
		$this->set_prop( 'padding_duration', absint( $value ) );
	}

	/**
	 * Returns the unit for the minimum lead time (minute, hour, day, month).
	 *
	 * @param  string $context
	 * @return string
	 */
	public function get_min_date_unit( string $context = 'view' ): string {
		return (string) $this->get_prop( 'min_date_unit', $context );
	}

	/**
	 * Set min_date_unit.
	 *
	 * @param string $value
	 */
	public function set_min_date_unit( $value ): void {
		$this->set_prop( 'min_date_unit', (string) $value );
	}

	/**
	 * Returns the minimum lead time value.
	 *
	 * @param  string $context
	 * @return integer
	 */
	public function get_min_date( string $context = 'view' ): int {
		return (int) $this->get_prop( 'min_date', $context );
	}

	/**
	 * Set min_date.
	 *
	 * @param integer $value
	 */
	public function set_min_date( $value ): void {
		$this->set_prop( 'min_date', absint( $value ) );
	}

	/**
	 * Returns the unit for the maximum lead time (minute, hour, day, month).
	 *
	 * @param  string $context
	 * @return string
	 */
	public function get_max_date_unit( string $context = 'view' ): string {
		return (string) $this->get_prop( 'max_date_unit', $context );
	}

	/**
	 * Set max_date_unit.
	 *
	 * @param string $value
	 */
	public function set_max_date_unit( $value ): void {
		$this->set_prop( 'max_date_unit', (string) $value );
	}

	/**
	 * Get max_date.
	 *
	 * @param  string $context
	 * @return integer
	 */
	public function get_max_date( string $context = 'view' ): int {
		return (int) $this->get_prop( 'max_date', $context );
	}

	/**
	 * Set max_date.
	 *
	 * @param integer $value
	 */
	public function set_max_date( $value ): void {
		$this->set_prop( 'max_date', absint( $value ) );
	}

	/**
	 * Returns whether the user can cancel the appointment.
	 *
	 * @param  string $context
	 * @return boolean
	 */
	public function get_user_can_cancel( string $context = 'view' ): bool {
		return (bool) $this->get_prop( 'user_can_cancel', $context );
	}

	/**
	 * Set user_can_cancel.
	 *
	 * @param boolean $value
	 */
	public function set_user_can_cancel( $value ): void {
		$this->set_prop( 'user_can_cancel', wc_appointments_string_to_bool( $value ) );
	}

	/**
	 * Returns the unit for the cancellation deadline (minute, hour, day, month).
	 *
	 * @param  string $context
	 * @return string
	 */
	public function get_cancel_limit_unit( string $context = 'view' ): string {
		$value = $this->get_prop( 'cancel_limit_unit', $context );

		if ('view' === $context) {
            return (string) apply_filters( 'woocommerce_appointments_get_cancel_limit_unit', $value, $this );
        }

		return (string) $value;
	}

	/**
	 * Set cancel_limit_unit.
	 *
	 * @param string $value
	 */
	public function set_cancel_limit_unit( $value ): void {
		$value = in_array( $value, [ self::DURATION_UNIT_MONTH, self::DURATION_UNIT_DAY, self::DURATION_UNIT_HOUR, self::DURATION_UNIT_MINUTE ], true ) ? $value : self::DURATION_UNIT_DAY;
		$this->set_prop( 'cancel_limit_unit', $value );
	}

	/**
	 * Returns the cancellation deadline value.
	 *
	 * @param  string $context
	 * @return integer
	 */
	public function get_cancel_limit( string $context = 'view' ): int {
		$value = $this->get_prop( 'cancel_limit', $context );

		if ('view' === $context) {
            return (int) apply_filters( 'woocommerce_appointments_get_cancel_limit', $value, $this );
        }

		return (int) $value;
	}

	/**
	 * Set cancel_limit.
	 *
	 * @param integer $value
	 */
	public function set_cancel_limit( $value ): void {
		$this->set_prop( 'cancel_limit', max( 1, absint( $value ) ) );
	}

	/**
	 * Get user_can_reschedule.
	 *
	 * @param  string $context
	 * @return boolean
	 */
	public function get_user_can_reschedule( string $context = 'view' ): bool {
		return (bool) $this->get_prop( 'user_can_reschedule', $context );
	}

	/**
	 * Set user_can_reschedule.
	 *
	 * @param boolean $value
	 */
	public function set_user_can_reschedule( $value ): void {
		$this->set_prop( 'user_can_reschedule', wc_appointments_string_to_bool( $value ) );
	}

	/**
	 * Returns the unit for the rescheduling deadline (minute, hour, day, month).
	 *
	 * @param  string $context
	 * @return string
	 */
	public function get_reschedule_limit_unit( string $context = 'view' ): string {
		return (string) $this->get_prop( 'reschedule_limit_unit', $context );
	}

	/**
	 * Set reschedule_limit_unit.
	 *
	 * @param string $value
	 */
	public function set_reschedule_limit_unit( $value ): void {
		$value = in_array( $value, [ self::DURATION_UNIT_MONTH, self::DURATION_UNIT_DAY, self::DURATION_UNIT_HOUR, self::DURATION_UNIT_MINUTE ], true ) ? $value : self::DURATION_UNIT_DAY;
		$this->set_prop( 'reschedule_limit_unit', $value );
	}

	/**
	 * Returns the rescheduling deadline value.
	 *
	 * @param  string $context
	 * @return integer
	 */
	public function get_reschedule_limit( string $context = 'view' ): int {
		return (int) $this->get_prop( 'reschedule_limit', $context );
	}

	/**
	 * Set reschedule_limit.
	 *
	 * @param integer $value
	 */
	public function set_reschedule_limit( $value ): void {
		$this->set_prop( 'reschedule_limit', max( 1, absint( $value ) ) );
	}

	/**
	 * Returns whether the appointment requires admin confirmation.
	 *
	 * @param  string $context
	 * @return boolean
	 */
	public function get_requires_confirmation( string $context = 'view' ): bool {
		return (bool) $this->get_prop( 'requires_confirmation', $context );
	}

	/**
	 * Set requires_confirmation.
	 *
	 * @param boolean $value
	 */
	public function set_requires_confirmation( $value ): void {
		$this->set_prop( 'requires_confirmation', wc_appointments_string_to_bool( $value ) );
	}

	/**
	 * Returns whether customer timezones are enabled.
	 *
	 * @param  string $context
	 * @return boolean
	 */
	public function get_customer_timezones( string $context = 'view' ): bool {
		return (bool) $this->get_prop( 'customer_timezones', $context );
	}

	/**
	 * Set customer_timezones.
	 *
	 * @param boolean $value
	 */
	public function set_customer_timezones( $value ): void {
		$this->set_prop( 'customer_timezones', wc_appointments_string_to_bool( $value ) );
	}

	/**
	 * Get cal_color.
	 *
	 * @param  string $context
	 * @return string
	 */
	public function get_cal_color( string $context = 'view' ): string {
		return (string) $this->get_prop( 'cal_color', $context );
	}

	/**
	 * Set get_cal_color.
	 *
	 * @param string $value
	 */
	public function set_cal_color( $value ): void {
		$this->set_prop( 'cal_color', $value );
	}

	/**
	 * Returns the availability span setting.
	 *
	 * @param  string $context
	 * @return string
	 */
	public function get_availability_span( string $context = 'view' ): string {
		$value = $this->get_prop( 'availability_span', $context );

		if ('view' === $context) {
            return (string) apply_filters( 'woocommerce_appointments_get_availability_span', $value, $this );
        }
		return (string) $value;
	}

	/**
	 * Set availability_span.
	 *
	 * @param string $value
	 */
	public function set_availability_span( $value ): void {
		$this->set_prop( 'availability_span', (string) $value );
	}

	/**
	 * Returns whether availability should be auto-selected.
	 *
	 * @param  string $context
	 * @return boolean
	 */
	public function get_availability_autoselect( string $context = 'view' ): bool {
		return (bool) $this->get_prop( 'availability_autoselect', $context );
	}

	/**
	 * Set availability_autoselect.
	 *
	 * @param boolean $value
	 */
	public function set_availability_autoselect( $value ): void {
		$this->set_prop( 'availability_autoselect', wc_appointments_string_to_bool( $value ) );
	}

	/**
	 * Returns the availability rules for the product.
	 *
	 * @param  string $context
	 * @return array
	 */
	public function get_availability( string $context = 'view' ): array {
		$product_rules = WC_Data_Store::load( 'appointments-availability' )->get_all_as_array(
		    [
				[
					'key'     => 'kind',
					'compare' => '=',
					'value'   => 'availability#product',
				],
				[
					'key'     => 'kind_id',
					'compare' => '=',
					'value'   => $this->get_id(),
				],
			],
		);

		return (array) apply_filters( 'wc_appointments_product_availability', $product_rules, $this );
	}

	/**
	 * Get has_restricted_days.
	 *
	 * @param  string $context
	 * @return string
	 */
	public function get_has_restricted_days( string $context = 'view' ): bool {
		return (bool) $this->get_prop( 'has_restricted_days', $context );
	}

	/**
	 * Set has_restricted_days.
	 *
	 * @param string $value
	 */
	public function set_has_restricted_days( $value ): void {
		$this->set_prop( 'has_restricted_days', $value );
	}

	/**
	 * Returns the restricted days.
	 *
	 * @param  string $context
	 * @return string
	 */
	public function get_restricted_days( string $context = 'view' ): array {
		return (array) $this->get_prop( 'restricted_days', $context );
	}

	/**
	 * Set restricted_days.
	 *
	 * @param string $value
	 */
	public function set_restricted_days( $value ): void {
		$this->set_prop( 'restricted_days', $value );
	}

	/**
	 * Returns the label used for staff.
	 *
	 * @param  string $context
	 * @return string
	 */
	public function get_staff_label( string $context = 'view' ): string {
		return (string) $this->get_prop( 'staff_label', $context );
	}

	/**
	 * Set staff_label.
	 *
	 * @param string $value
	 */
	public function set_staff_label( $value ): void {
		$this->set_prop( 'staff_label', $value );
	}

	/**
	 * Get staff_assignment.
	 *
	 * @param  string $context
	 * @return string
	 */
	public function get_staff_assignment( string $context = 'view' ): string {
		return (string) $this->get_prop( 'staff_assignment', $context );
	}

	/**
	 * Set staff_assignment.
	 *
	 * @param string $value
	 */
	public function set_staff_assignment( $value ): void {
		$this->set_prop( 'staff_assignment', (string) $value );
	}

	/**
	 * Returns whether 'no preference' option is available for staff.
	 *
	 * @param  string $context
	 * @return string
	 */
	public function get_staff_nopref( string $context = 'view' ): bool {
		return (bool) $this->get_prop( 'staff_nopref', $context );
	}

	/**
	 * Set staff_nopref.
	 *
	 * @param string $value
	 */
	public function set_staff_nopref( $value ): void {
		$this->set_prop( 'staff_nopref', wc_appointments_string_to_bool( $value ) );
	}

	/**
	 * Returns the list of assigned staff IDs.
	 *
	 * @param  string $context
	 * @return array
	 */
	public function get_staff_ids( string $context = 'view' ): array {
		return (array) $this->get_prop( 'staff_ids', $context );
	}

	/**
	 * Set staff_ids.
	 *
	 * @param array $value
	 */
	public function set_staff_ids( $value ): void {
		$this->set_prop( 'staff_ids', wp_parse_id_list( (array) $value ) );
	}

	/**
	 * Returns the base costs for staff.
	 *
	 * @param  string $context
	 * @return array
	 */
	public function get_staff_base_costs( string $context = 'view' ): array {
		return (array) $this->get_prop( 'staff_base_costs', $context );
	}

	/**
	 * Set staff_base_costs.
	 *
	 * @param array $value
	 */
	public function set_staff_base_costs( $value ): void {
		$this->set_prop( 'staff_base_costs', (array) $value );
	}

	/**
	 * Returns the quantities for staff.
	 *
	 * @param  string $context
	 * @return array
	 */
	public function get_staff_qtys( string $context = 'view' ): array {
		return (array) $this->get_prop( 'staff_qtys', $context );
	}

	/**
	 * Set staff_qtys.
	 *
	 * @param array $value
	 */
	public function set_staff_qtys( $value ): void {
		$this->set_prop( 'staff_qtys', (array) $value );
	}

	/*
	|--------------------------------------------------------------------------
	| Conditionals
	|--------------------------------------------------------------------------
	|
	| Conditionals functions which return true or false.
	*/

	/**
	 * If this product class is a skeleton/place holder class (used for appointment addons).
	 *
	 * @return boolean
	 */
	public function is_skeleton(): bool {
		return false;
	}

	/**
	 * If this product class is an addon for appointments.
	 *
	 * @return boolean
	 */
	public function is_appointments_addon(): bool {
		return false;
	}

	/**
	 * Extension/plugin/add-on name for the appointment addon this product refers to.
	 *
	 * @return string
	 */
	public function appointments_addon_title(): string {
		return '';
	}

	/**
	 * Returns whether or not the product is in stock.
	 *
	 * @todo Develop further to embrace WC stock statuses and backorders.
	 *
	 * @return bool
	 */
	public function is_in_stock() {
		return apply_filters( 'woocommerce_product_is_in_stock', true, $this );
		// return apply_filters( 'woocommerce_product_is_in_stock', 'instock' === $this->get_stock_status(), $this );
	}

	/**
	 * Appointments can always be purchased regardless of price.
	 *
	 * @return boolean
	 */
	public function is_purchasable() {
		return apply_filters( 'woocommerce_is_purchasable', $this->exists() && ( 'publish' === $this->get_status() || current_user_can( 'edit_post', $this->get_id() ) ), $this );
	}

	/**
	 * The base cost will either be the 'base' cost or the base cost + cheapest staff
	 * @return string
	 */
	public function get_base_cost(): float {
		$base = $this->get_price();

		if ( $this->has_staff() ) {
			$staff    = $this->get_staff();
			$cheapest = null;

			foreach ( $staff as $staff_member ) {
				if ( is_null( $cheapest ) || $staff_member->get_base_cost() < $cheapest ) {
					$cheapest = $staff_member->get_base_cost();
				}
			}
			$base += $cheapest;
		}

		return (float) $base;
	}

	/**
	 * Return if appointment has extra costs.
	 *
	 * @return bool
	 */
	public function has_additional_costs(): bool {
		if ( $this->get_has_additional_costs() ) {
			return true;
		}

		if ( $this->has_staff() ) {
			foreach ( (array) $this->get_staff() as $staff_member ) {
				if ( $staff_member->get_base_cost() ) {
					return true;
				}
			}
		}

		$costs = $this->get_costs();
        return ! empty( $costs ) && $this->get_has_pricing();
	}

	/**
	 * How staff are assigned.
	 *
	 * @param string $type
	 * @return boolean customer or automatic
	 */
	public function is_staff_assignment_type( string $type ): bool {
		return $this->get_staff_assignment() === $type;
	}

	/**
	 * Checks if a product requires confirmation.
	 *
	 * @return bool
	 */
	public function requires_confirmation(): bool {
		return (bool) apply_filters( 'woocommerce_appointment_requires_confirmation', $this->get_requires_confirmation(), $this );
	}

	/**
	 * See if the appointment can be cancelled.
	 *
	 * @return boolean
	 */
	public function can_be_cancelled(): bool {
		return (bool) apply_filters( 'woocommerce_appointment_user_can_cancel', $this->get_user_can_cancel(), $this );
	}

	/**
	 * See if the appointment can be rescheduled.
	 *
	 * @return boolean
	 */
	public function can_be_rescheduled(): bool {
		return (bool) apply_filters( 'woocommerce_appointment_user_can_reschedule', $this->get_user_can_reschedule(), $this );
	}

	/**
	 * See if the appointment has timezones.
	 *
	 * @return boolean
	 */
	public function has_timezones(): bool {
		return (bool) apply_filters( 'woocommerce_appointment_customer_timezones', $this->get_customer_timezones(), $this );
	}

	/**
	 * See if the appointment requires time selection.
	 *
	 * @return boolean
	 */
	public function has_time(): bool {
		return in_array( $this->get_duration_unit(), [ self::DURATION_UNIT_HOUR, self::DURATION_UNIT_MINUTE ], true );
	}

	/**
	 * See if dates are by default appointable.
	 *
	 * @return bool
	 */
	public function get_default_availability(): bool {
		return (bool) apply_filters( 'woocommerce_appointment_default_availability', false, $this );
	}

	/**
	 * See if this appointment product has restricted days.
	 *
	 * @return boolean
	 */
	public function has_restricted_days(): bool {
		return $this->get_has_restricted_days();
	}

	/*
	|--------------------------------------------------------------------------
	| Non-CRUD getters
	|--------------------------------------------------------------------------
	*/
	/**
	 * Gets all formatted cost rules.
	 *
	 * @return array
	 */
	public function get_costs(): array {
		if ( ! $this->get_has_pricing() ) {
			return [];
		}
		return WC_Product_Appointment_Rule_Manager::process_pricing_rules( $this->get_pricing() );
	}

	/**
	 * Get Min date.
	 *
	 * @return array|bool
	 */
	public function get_min_date_a(): array {
        return ['value' => apply_filters( 'woocommerce_appointments_min_date', $this->get_min_date(), $this->get_id() ), 'unit' => $this->get_min_date_unit() ? apply_filters( 'woocommerce_appointments_min_date_unit', $this->get_min_date_unit(), $this->get_id() ) : 'month'];
    }

	/**
	 * Get max date.
	 *
	 * @return array
	 */
	public function get_max_date_a(): array {
        return ['value' => $this->get_max_date() ? apply_filters( 'woocommerce_appointments_max_date', $this->get_max_date(), $this->get_id() ) : 1, 'unit' => $this->get_max_date_unit() ? apply_filters( 'woocommerce_appointments_max_date_unit', $this->get_max_date_unit(), $this->get_id() ) : 'month'];
    }

	/**
	 * Get default intervals.
	 *
	 * @since 3.2.0 introduced.
	 * @param  int $id
	 * @return Array
	 */
	public function get_intervals(): array {
		$default_interval = self::DURATION_UNIT_HOUR === $this->get_duration_unit() ? $this->get_duration() * 60 : $this->get_duration();
		$custom_interval  = self::DURATION_UNIT_HOUR === $this->get_duration_unit() ? $this->get_duration() * 60 : $this->get_duration();
		if ( $this->get_interval_unit() && $this->get_interval() ) {
			$custom_interval = self::DURATION_UNIT_HOUR === $this->get_interval_unit() ? $this->get_interval() * 60 : $this->get_interval();
			$custom_interval = self::DURATION_UNIT_MONTH === $this->get_duration_unit() ? $this->get_duration() : $custom_interval;
		}

		// Filters for the intervals.
		$default_interval = apply_filters( 'woocommerce_appointments_interval', $default_interval, $this );
		$custom_interval  = apply_filters( 'woocommerce_appointments_base_interval', $custom_interval, $this );

		return [ $default_interval, $custom_interval ];
	}

	/**
	 * See if this appointment product has any staff.
	 * @return boolean
	 */
	public function has_staff(): bool {
		$count_staff = count( $this->get_staff_ids() );
		return (bool) $count_staff;
	}

	/**
	 * Get staff by ID.
	 *
	 * @param  int $id
	 * @return WC_Product_Appointment_Staff object
	 */
	public function get_staff(): array {
		$product_staff = [];

		foreach ( $this->get_staff_ids() as $staff_id ) {
			$product_staff[] = new WC_Product_Appointment_Staff( $staff_id, $this );
		}

		return $product_staff;
	}

	/**
	 * Get staff member by ID
	 *
	 * @param  int $id
	 * @return WC_Product_Appointment_Staff object
	 */
	public function get_staff_member( int $staff_id ) {
		if ( $this->has_staff() && ! empty( $staff_id ) ) {
			return new WC_Product_Appointment_Staff( $staff_id, $this );
		}

		return false;
	}

	/**
	 * Get staff members by IDs
	 *
	 * @param  int $id
	 * @param  bool $names
	 * @param  bool $with_link
	 * @return WC_Product_Appointment_Staff object
	 */
	public function get_staff_members( $ids = [], bool $names = false, bool $with_link = false ) {
		// If no IDs are give, get all product staff IDs.
		if ( ! $ids ) {
			$ids = $this->get_staff_ids();

			return false;
		}

		return wc_appointments_get_staff_from_ids( $ids, $names, $with_link );
	}

	/**
	 * Get available quantity.
	 *
	 * @since 3.2.0 introduced.
	 * @param $staff_id
	 * @return bool|int
	 */
	public function get_available_qty( $staff_id = '', bool $no_fallback = false, bool $individual = false ): int {
		$default_qty = $no_fallback ? 0 : $this->get_qty();

		if ( $this->has_staff() ) {
			$qtys       = $this->get_staff_qtys();
			$staff_qty  = 0;
			$staff_qtys = [];

			if ( $staff_id && is_array( $staff_id ) ) {
				foreach ( $staff_id as $staff_member_id ) {
					$qty          = isset( $qtys[ $staff_member_id ] ) && '' !== $qtys[ $staff_member_id ] && 0 !== $qtys[ $staff_member_id ] ? $qtys[ $staff_member_id ] : $default_qty;
					$staff_qtys[] = $qty;
				}
				// Only count when $qtys is an array.
				$staff_qty = $this->is_staff_assignment_type( 'all' ) ? max( $staff_qtys ) : array_sum( $staff_qtys );
			} elseif ( $staff_id && is_numeric( $staff_id ) ) {
				$staff_qty = isset( $qtys[ $staff_id ] ) && '' !== $qtys[ $staff_id ] && 0 !== $qtys[ $staff_id ] ? $qtys[ $staff_id ] : $default_qty;
			} elseif ( ! $staff_id ) {
				foreach ( $this->get_staff_ids() as $staff_member_id ) {
					$staff_qtys[] = isset( $qtys[ $staff_member_id ] ) && '' !== $qtys[ $staff_member_id ] && 0 !== $qtys[ $staff_member_id ] ? $qtys[ $staff_member_id ] : $this->get_qty();
				}
				// Only count when $qtys is an array.
				if ( [] !== $staff_qtys ) {
					$staff_qty = $individual || $this->is_staff_assignment_type( 'all' ) ? max( $staff_qtys ) : array_sum( $staff_qtys );
				}
			}

			return (int) apply_filters( 'woocommerce_appointments_get_available_quantity', $staff_qty ? absint( $staff_qty ) : absint( $default_qty ), $this, $staff_id );
		}

		return (int) apply_filters( 'woocommerce_appointments_get_available_quantity', $default_qty, $this, $staff_id );
	}

	/**
	 * Get rules in order of `override power`. The higher the index the higher the override power. Element at index 4 will
	 * override element at index 2.
	 *
	 * Within priority the rules will be ordered top to bottom.
	 *
	 * @return array  availability_rules {
	 *    @type $staff_id => array {
	 *
	 *       The $order_index depicts the levels override. `0` Is the lowest. `1` overrides `0` and `2` overrides `1`.
	 *       e.g. If monday is set to available in `1` and not available in `2` the results should be that Monday is
	 *       NOT available because `2` overrides `1`.
	 *       $order_index corresponds to override power. The higher the element index the higher the override power.
	 *       @type $order_index => array {
	 *          @type string $type   The type of range selected in admin.
	 *          @type string $range  Depending on the type this depicts what range and if available or not.
	 *          @type integer $priority
	 *          @type string $level Global, Product or Staff
	 *          @type integer $order The index for the order set in admin.
	 *      }
	 * }
	 */
	public function get_availability_rules( $for_staff = 0 ): array {
		// Default to zero, when no staff is set.
		if ( empty( $for_staff ) ) {
			$for_staff = 0;
		}

		// Repeat the function if staff IDs are in array.
		if ( is_array( $for_staff ) ) {
			$for_all_staff = [];
			foreach ( $for_staff as $for_staff_id ) {
				$for_all_staff[] = $this->get_availability_rules( $for_staff_id );
			}
			return array_merge( ...$for_all_staff );
		}

		if ( ! isset( $this->availability_rules[ $for_staff ] ) ) {
			$this->availability_rules[ $for_staff ] = [];

			// Global and Gcal rules.
			$global_rules = WC_Appointments_Availability_Data_Store::get_global_availability();
			#print '<pre>'; print_r( $global_rules ); print '</pre>';

			// Product rules.
			$product_rules = $this->get_availability();

			#print '<pre>'; print_r( $product_rules ); print '</pre>';

			// Staff rules.
			$staff_rules = [];

			// Get availability of each staff - no staff has been chosen yet.
			if ( $this->has_staff() && ! $for_staff ) {
				// If all slotss are available by default, we should not hide days if we don't know which staff is going to be used.
				if ( ! $this->get_default_availability() ) {
					// Staff rules.
					$staff_rules = WC_Appointments_Availability_Data_Store::get_staff_availability( $this->get_staff_ids() );
					#print '<pre>'; print_r( $staff_rules ); print '</pre>';
				}
			} elseif ( $for_staff ) {

				// Staff rules.
				$staff_object = $this->get_staff_member( $for_staff );
				$staff_rules  = $staff_object ? $staff_object->get_availability() : [];
				#print '<pre>'; print_r( $staff_rules ); print '</pre>';
			}

			// The order that these rules are put into the array are important due to the way that
			// the rules as processed for overrides.
			$availability_rules = array_filter(
			    array_merge(
			        WC_Product_Appointment_Rule_Manager::process_availability_rules( array_reverse( $global_rules ), 'global', true, $this ),
			        WC_Product_Appointment_Rule_Manager::process_availability_rules( array_reverse( $product_rules ), 'product', true, $this ),
			        WC_Product_Appointment_Rule_Manager::process_availability_rules( array_reverse( $staff_rules ), 'staff', true, $this ),
			    ),
			);
			#print '<pre>'; print_r( $availability_rules ); print '</pre>';

			usort( $availability_rules, [ $this, 'rule_override_power_sort' ] );

			$this->availability_rules[ $for_staff ] = $availability_rules;
		}

		return apply_filters( 'woocommerce_appointment_get_availability_rules', $this->availability_rules[ $for_staff ], $for_staff, $this );
	}

	/*
	|--------------------------------------------------------------------------
	| Slot calculation functions. @todo move to own manager class
	|--------------------------------------------------------------------------
	*/

	/**
	 * Check the staff availability against all the slots.
	 *
	 * @param  string $start_date
	 * @param  string $end_date
	 * @param  int    $qty
	 * @param  WC_Product_Appointment_Staff|null $product_staff
	 * @return string|WP_Error
	 */
	public function get_slots_availability( int $start_date, int $end_date, int $qty, $staff_id, array $intervals = [] ) {
		$slots              = $this->get_slots_in_range( $start_date, $end_date, $intervals, $staff_id, [], false, false );
		$interval           = $this->get_duration_in_minutes();

		if ( empty( $slots ) || ! in_array( $start_date, $slots ) ) {
			return false;
		}

		#print '<pre>'; print_r( date( 'Ymd H:i', $start_date ) . ' ======= ' . date( 'Ymd H:i', $end_date ) ); print '</pre>';
		#print '<pre>'; print_r( $slots ); print '</pre>';
		#print '<pre>'; print_r( $staff_id ); print '</pre>';

		$available_qtys         = [];
		$original_available_qty = $this->get_available_qty( $staff_id );

		#print '<pre>'; print_r( $slots ); print '</pre>';

		// Get appointments.
		$existing_appointments = WC_Appointment_Data_Store::get_appointments_in_date_range( $start_date, $end_date, $this->get_id(), $staff_id );

		// Check all slots availability.
		foreach ( $slots as $slot ) {
			$qty_scheduled_in_slot = 0;

			// Check capacity based on duration unit.
			if ( in_array( $this->get_duration_unit(), [ self::DURATION_UNIT_HOUR, self::DURATION_UNIT_MINUTE ], true ) ) {
				$slot_qty = WC_Product_Appointment_Rule_Manager::check_availability_rules_against_time( $this, $slot, strtotime( "+{$interval} minutes", $slot ), $staff_id, true );
			} else {
				$slot_qty = WC_Product_Appointment_Rule_Manager::check_availability_rules_against_date( $this, $slot, $staff_id, true );
			}

			#print '<pre>'; print_r( date( 'G:i', $slot ) . '___' .'_qty:'. $slot_qty .'__qty_orig:'. $original_available_qty ); print '</pre>';
			#print '<pre>'; print_r( $existing_appointments ); print '</pre>';

			if ( ! empty( $existing_appointments ) ) {
				foreach ( $existing_appointments as $existing_appointment ) {
					// Appointment and Slot start/end timestamps.
					$slot_start        = $slot;
					$slot_end          = strtotime( "+{$interval} minutes", $slot );
					$appointment_start = $existing_appointment->get_start();
					$appointment_end   = $existing_appointment->get_end();

					// Product padding?
					$padding_duration_in_minutes = $this->get_padding_duration_in_minutes();
					if ( $padding_duration_in_minutes && in_array( $this->get_duration_unit(), [ self::DURATION_UNIT_HOUR, self::DURATION_UNIT_MINUTE, self::DURATION_UNIT_DAY ], true ) ) {
						$appointment_start = strtotime( "-{$padding_duration_in_minutes} minutes", $appointment_start );
						$appointment_end   = strtotime( "+{$padding_duration_in_minutes} minutes", $appointment_end );
					}
                    // Is within slot?
                    if (! $appointment_start) {
                        continue;
                    }
                    if (! $appointment_end) {
                        continue;
                    }
                    if ($appointment_start >= $slot_end) {
                        continue;
                    }
                    if ($appointment_end <= $slot_start) {
                        continue;
                    }

					// When existing appointment is scheduled with another product,
					// remove all available capacity, so staff becomes unavailable for this product.
					if ( $existing_appointment->get_product_id() !== $this->get_id() ) {
						$check = apply_filters( 'wc_appointments_check_appointment_product', true, $existing_appointment->get_id(), $this->get_id() );
						$check = apply_filters( 'wc_apointments_check_appointment_product', true, $existing_appointment->get_id(), $this->get_id() ); // BC for old hook name
						if ( $check ) {
							$qty_to_add = $original_available_qty;
						} else {
							$qty_to_add = $existing_appointment->get_qty() ?: 1;
						}
					// Only remove capacity scheduled for existing product.
					} else {
						$qty_to_add = $existing_appointment->get_qty() ?: 1;
					}
					$qty_to_add = apply_filters( 'wc_appointments_check_appointment_qty', $qty_to_add, $existing_appointment, $this );

					$qty_scheduled_in_slot += $qty_to_add;

					// Staff doesn't match, so don't check.
					if ( $staff_id
						&& ! is_array( $staff_id )
					    && $existing_appointment->get_staff_ids()
						&& is_array( $existing_appointment->get_staff_ids() )
						&& ! in_array( $staff_id, $existing_appointment->get_staff_ids() ) ) {

						$qty_scheduled_in_slot -= $qty_to_add;

					} elseif ( $staff_id
						&& is_array( $staff_id )
					    && $existing_appointment->get_staff_ids()
						&& is_array( $existing_appointment->get_staff_ids() )
					 	&& ! array_intersect( $staff_id, $existing_appointment->get_staff_ids() ) ) {

						$qty_scheduled_in_slot -= $qty_to_add;

					}
				}
			}

			// Calculate available capacity.
			$available_qty = max( $slot_qty - $qty_scheduled_in_slot, 0 );

			#print '<pre>'; print_r( date( 'ymd H:i', $slot ) . '____' . $available_qty .' = '. $slot_qty .' - '. $qty_scheduled_in_slot . ' < ' . $qty . ' staff=' . $staff_id ); print '</pre>';

			// Remaining places are less than requested qty, return an error.
			if ($available_qty < $qty) {
                if ( in_array( $this->get_duration_unit(), [ self::DURATION_UNIT_HOUR, self::DURATION_UNIT_MINUTE ], true ) ) {
                    return new WP_Error(
                        'Error',
                        sprintf(
							/* translators: %1$d: available quantity %2$s: appointment slot date %3$s: appointment slot time */
							_n( 'There is a maximum of %1$d place remaining on %2$s at %3$s.', 'There are a maximum of %1$d places remaining on %2$s at %3$s.', $available_qty, 'woocommerce-appointments' ),
                            max( $available_qty, 0 ),
                            date_i18n( wc_appointments_date_format(), $slot ),
                            date_i18n( wc_appointments_time_format(), $slot ),
                        ),
                    );
                }
                if ([] === $available_qtys) {
                    return new WP_Error(
                        'Error',
                        sprintf(
							/* translators: %1$d: available quantity %2$s: appointment slot date */
							_n( 'There is a maximum of %1$d place remaining on %2$s', 'There are a maximum of %1$d places remaining on %2$s', $available_qty, 'woocommerce-appointments' ),
                            $available_qty,
                            date_i18n( wc_appointments_date_format(), $slot ),
                        ),
                    );
                }
                return new WP_Error(
                    'Error',
                    sprintf(
							/* translators: %1$d: available quantity %2$s: appointment slot date */
							_n( 'There is a maximum of %1$d place remaining on %2$s', 'There are a maximum of %1$d places remaining on %2$s', $available_qty, 'woocommerce-appointments' ),
                        max( $available_qtys ),
                        date_i18n( wc_appointments_date_format(), $slot ),
                    ),
                );
            }

			$available_qtys[] = $available_qty;
		}

		return apply_filters(
		    'woocommerce_appointments_slots_availability',
		    min( $available_qtys ),
		    $qty,
		    $start_date,
		    $end_date,
		    $staff_id,
		    $this,
		);
	}

	/**
	 * Get an array of slots within in a specified date range - might be days, might be slots within days, depending on settings.
	 *
	 * @param       $start_date
	 * @param       $end_date
	 * @param array $intervals
	 * @param int   $staff_id
	 * @param array $scheduled
	 * @param bool  $get_past_times
	 *
	 * @return array
	 */
	public function get_slots_in_range( int $start_date, int $end_date, array $intervals = [], $staff_id = 0, array $scheduled = [], bool $get_past_times = false, bool $timezone_span = true ): array {
		$intervals = empty( $intervals ) ? $this->get_intervals() : $intervals;

		// Span 1 day before and after to account for all timezones.
		if ( $timezone_span ) {
			$start_date = strtotime( '-1 day', $start_date );
			$end_date   = strtotime( '+1 day', $end_date );
		}

		#print '<pre>'; print_r( debug_backtrace() ); print '</pre>';
		#print '<pre>'; print_r( date( 'Y-n-j H:i', $start_date ) .'___'. date( 'Y-n-j H:i', $end_date ) ); print '</pre>';
		#print '<pre>'; print_r( $staff_id ); print '</pre>';

		if ($this->has_staff() && 0 === $staff_id) {
            $staff_ids      = $this->get_staff_ids();
            $slots_in_range = [];
            foreach ( $staff_ids as $staff_id ) {
				if ( self::DURATION_UNIT_DAY === $this->get_duration_unit() ) {
					$slots_in_range_a = $this->get_slots_in_range_for_day( $start_date, $end_date, $staff_id, $scheduled );
				} elseif ( self::DURATION_UNIT_MONTH === $this->get_duration_unit() ) {
					$slots_in_range_a = $this->get_slots_in_range_for_month( $start_date, $end_date, $staff_id );
				} else {
					$slots_in_range_a = $this->get_slots_in_range_for_hour_or_minutes( $start_date, $end_date, $intervals, $staff_id, $scheduled, $get_past_times );
				}

				#print '<pre>'; print_r( $staff_id ); print '</pre>';
				#print '<pre>'; print_r( $slots_in_range ); print '</pre>';

				$slots_in_range = array_merge( $slots_in_range_a, $slots_in_range );
			}
        } elseif ( self::DURATION_UNIT_DAY === $this->get_duration_unit() ) {
            $slots_in_range = $this->get_slots_in_range_for_day( $start_date, $end_date, $staff_id, $scheduled );
        } elseif ( self::DURATION_UNIT_MONTH === $this->get_duration_unit() ) {
				$slots_in_range = $this->get_slots_in_range_for_month( $start_date, $end_date, $staff_id );
			} else {
				$slots_in_range = $this->get_slots_in_range_for_hour_or_minutes( $start_date, $end_date, $intervals, $staff_id, $scheduled, $get_past_times );
			}

		#print '<pre>'; print_r( $slots_in_range ); print '</pre>';

		asort( $slots_in_range ); #sort ascending by value so latest time goes at the end

		#print '<pre>'; print_r( $slots_in_range ); print '</pre>';

		return array_unique( $slots_in_range );
	}

	/**
	 * Get slots in range with lazy generation - stops at first available slot.
	 * Optimized version for finding just the first available slot without generating all slots.
	 *
	 * @param int $start_date Start timestamp
	 * @param int $end_date End timestamp
	 * @param array $intervals Intervals array
	 * @param int $staff_id Staff ID
	 * @param array $scheduled Scheduled appointments
	 * @param bool $get_past_times Whether to get past times
	 * @param bool $timezone_span Whether to span timezones
	 * @param int $max_slots Maximum slots to generate (0 = unlimited)
	 * @return array|false Array of slots or false if none found
	 */
	public function get_slots_in_range_lazy( int $start_date, int $end_date, array $intervals = [], $staff_id = 0, array $scheduled = [], bool $get_past_times = false, bool $timezone_span = true, int $max_slots = 1 ): array|false {
		$intervals = empty( $intervals ) ? $this->get_intervals() : $intervals;
		$found_slots = [];

		// Span 1 day before and after to account for all timezones.
		if ( $timezone_span ) {
			$start_date = strtotime( '-1 day', $start_date );
			$end_date   = strtotime( '+1 day', $end_date );
		}

		if ( $this->has_staff() && 0 === $staff_id ) {
			$staff_ids = $this->get_staff_ids();

			foreach ( $staff_ids as $staff_id ) {
				$staff_slots = $this->get_slots_lazy_by_duration( $start_date, $end_date, $intervals, $staff_id, $scheduled, $get_past_times, $max_slots - count( $found_slots ) );
				$found_slots = array_merge( $found_slots, $staff_slots );

				// Early exit if we have enough slots
				if ( 0 < $max_slots && count( $found_slots ) >= $max_slots ) {
					break;
				}
			}
		} else {
			$found_slots = $this->get_slots_lazy_by_duration( $start_date, $end_date, $intervals, $staff_id, $scheduled, $get_past_times, $max_slots );
		}

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

		asort( $found_slots );
		return array_unique( array_slice( $found_slots, 0, $max_slots ) );
	}

	/**
     * Helper method to get slots lazily based on duration unit.
     *
     * @param int $start_date Start timestamp
     * @param int $end_date End timestamp
     * @param array $intervals Intervals array
     * @param int $staff_id Staff ID
     * @param array $scheduled Scheduled appointments
     * @param bool $get_past_times Whether to get past times
     * @param int $max_slots Maximum slots to find
     */
    private function get_slots_lazy_by_duration( $start_date, $end_date, $intervals, $staff_id, $scheduled, $get_past_times, $max_slots ): array {
		if ( self::DURATION_UNIT_DAY === $this->get_duration_unit() ) {
            return $this->get_slots_in_range_for_day_lazy( $start_date, $end_date, $staff_id, $scheduled, $max_slots );
        }
        if ( self::DURATION_UNIT_MONTH === $this->get_duration_unit() ) {
            return $this->get_slots_in_range_for_month_lazy( $start_date, $end_date, $staff_id, $max_slots );
        }
        return $this->get_slots_in_range_for_hour_or_minutes_lazy( $start_date, $end_date, $intervals, $staff_id, $scheduled, $get_past_times, $max_slots );
	}

	/**
     * Lazy version of get_slots_in_range_for_day - stops early when max_slots reached.
     *
     * @param int $start_date Start timestamp
     * @param int $end_date End timestamp
     * @param int $staff_id Staff ID
     * @param array $scheduled Scheduled appointments
     * @param int $max_slots Maximum slots to find
     */
    private function get_slots_in_range_for_day_lazy( $start_date, $end_date, $staff_id, $scheduled, $max_slots ): array {
		$slots = [];

		// get scheduled days with a counter to specify how many appointments on that date
		$scheduled_days_with_count = [];
		foreach ( $scheduled as $appointment ) {
			$appointment_start       = $appointment[0];
			$appointment_end         = $appointment[1];
			$current_appointment_day = $appointment_start;

			while ( $current_appointment_day < $appointment_end ) {
				$date = date( 'Y-m-d', $current_appointment_day );

				if ( isset( $scheduled_days_with_count[ $date  ] ) ) {
					$scheduled_days_with_count[ $date ]++;
				} else {
					$scheduled_days_with_count[ $date ] = 1;
				}

				$current_appointment_day = strtotime( '+1 day', $current_appointment_day );
			}
		}

		$check_date = $start_date;
		$end_date = $this->get_max_allowed_date_into_the_future( $end_date );

		while ( $check_date <= $end_date && ( 0 === $max_slots || count( $slots ) < $max_slots ) ) {
			if ( WC_Product_Appointment_Rule_Manager::check_availability_rules_against_date( $this, $check_date, $staff_id ) ) {
				$available_qty = WC_Product_Appointment_Rule_Manager::check_availability_rules_against_date( $this, $check_date, $staff_id, true );
				$date          = date( 'Y-m-d', $check_date );
				if ( ! isset( $scheduled_days_with_count[ $date ] ) || $scheduled_days_with_count[ $date ] < $available_qty ) {
					$slots[] = $check_date;
				}
			}

			$check_date = strtotime( '+1 day', $check_date );
		}

		return $slots;
	}

	/**
     * Lazy version for month duration - stops early when max_slots reached.
     *
     * @param int $start_date Start timestamp
     * @param int $end_date End timestamp
     * @param int $staff_id Staff ID
     * @param int $max_slots Maximum slots to find
     */
    private function get_slots_in_range_for_month_lazy( $start_date, $end_date, $staff_id, $max_slots ): array {
		// For now, delegate to original method as month optimization is complex
		// This can be optimized further if needed
		return array_slice( $this->get_slots_in_range_for_month( $start_date, $end_date, $staff_id ), 0, $max_slots );
	}

	/**
     * Lazy version for hour/minute duration - stops early when max_slots reached.
     *
     * @param int $start_date Start timestamp
     * @param int $end_date End timestamp
     * @param array $intervals Intervals array
     * @param int $staff_id Staff ID
     * @param array $scheduled Scheduled appointments
     * @param bool $get_past_times Whether to get past times
     * @param int $max_slots Maximum slots to find
     */
    private function get_slots_in_range_for_hour_or_minutes_lazy( $start_date, $end_date, $intervals, $staff_id, $scheduled, $get_past_times, $max_slots ): array {
		// For now, delegate to original method as hour/minute optimization is complex
		// This can be optimized further if needed
		return array_slice( $this->get_slots_in_range_for_hour_or_minutes( $start_date, $end_date, $intervals, $staff_id, $scheduled, $get_past_times ), 0, $max_slots );
	}

	/**
	 * Get slots/day slots in range for day duration unit.
	 *
	 * @param $start_date
	 * @param $end_date
	 * @param $staff_id
	 * @param $scheduled
	 *
	 * @return array
	 */
	public function get_slots_in_range_for_day( int $start_date, int $end_date, $staff_id, array $scheduled ): array {
		$slots = [];

		// get scheduled days with a counter to specify how many appointments on that date
		$scheduled_days_with_count = [];
		foreach ( $scheduled as $appointment ) {
			$appointment_start       = $appointment[0];
			$appointment_end         = $appointment[1];
			$current_appointment_day = $appointment_start;

			// < because appointment end depicts an end of a day and not a start for a new day.
			while ( $current_appointment_day < $appointment_end ) {
				$date = date( 'Y-m-d', $current_appointment_day );

				if ( isset( $scheduled_days_with_count[ $date  ] ) ) {
					$scheduled_days_with_count[ $date ]++;
				} else {
					$scheduled_days_with_count[ $date ] = 1;
				}

				$current_appointment_day = strtotime( '+1 day', $current_appointment_day );
			}
		}

		// If exists always treat scheduling_period in minutes.
		$check_date = $start_date;

		$end_date = $this->get_max_allowed_date_into_the_future( $end_date );

		while ( $check_date <= $end_date ) {
			if ( WC_Product_Appointment_Rule_Manager::check_availability_rules_against_date( $this, $check_date, $staff_id ) ) {
				$available_qty = WC_Product_Appointment_Rule_Manager::check_availability_rules_against_date( $this, $check_date, $staff_id, true );
				$date          = date( 'Y-m-d', $check_date );
				if ( ! isset( $scheduled_days_with_count[ $date ] ) || $scheduled_days_with_count[ $date ] < $available_qty ) {
					$slots[] = $check_date;
				}
			}

			// move to next day
			$check_date = strtotime( '+1 day', $check_date );
		}

		return $slots;
	}

	/**
	 * For months, loop each month in the range to find slots.
	 *
	 * @param $start_date
	 * @param $end_date
	 * @param integer $staff_id
	 *
	 * @return array
	 */
	public function get_slots_in_range_for_month( int $start_date, int $end_date, $staff_id ): array {
		$slots = [];

		if ( 'month' !== $this->get_duration_unit() ) {
			return $slots;
		}

		$end_date = $this->get_max_allowed_date_into_the_future( $end_date );

		// Generate a range of slots for months
		$from       = strtotime( date( 'Y-m-01', $start_date ) );
		$to         = strtotime( date( 'Y-m-t', $end_date ) );
		$month_diff = 0;
		$month_from = strtotime( '+1 MONTH', $from );

		while ( $month_from <= $to ) {
			$month_from = strtotime( '+1 MONTH', $month_from );
			$month_diff ++;
		}

		for ( $i = 0; $i <= $month_diff; $i ++ ) {
			$year  = date( 'Y', ( 0 !== $i ? strtotime( "+ {$i} month", $from ) : $from ) );
			$month = date( 'n', ( 0 !== $i ? strtotime( "+ {$i} month", $from ) : $from ) );

			if ( ! WC_Product_Appointment_Rule_Manager::check_availability_rules_against_date( $this, strtotime( "{$year}-{$month}-01" ), $staff_id, true ) ) {
				continue;
			}

			$slots[] = strtotime( "+ {$i} month", $from );
		}

		return $slots;
	}

	/**
	 * Get slots in range for hour or minute duration unit.
	 * For minutes and hours find valid slots within THIS DAY ($check_date)
	 *
	 * @param $start_date
	 * @param $end_date
	 * @param $intervals
	 * @param $staff_id
	 * @param $scheduled
	 * @param $get_past_times
	 *
	 * @return array
	 */
	public function get_slots_in_range_for_hour_or_minutes( int $start_date, int $end_date, array $intervals, $staff_id, array $scheduled, bool $get_past_times ): array {
		// Setup.
		$slot_start_times_in_range   = [];
		$minutes_not_available       = [];
		$interval                    = $intervals[0]; #duration
		$check_date                  = $start_date;
		$first_slot_time_minute      = 0;
		$default_appointable_minutes = $this->get_default_availability() ? range( $first_slot_time_minute, ( 1440 + $interval ) ) : [];
		$rules                       = $this->get_availability_rules( $staff_id ); // Work out what minutes are actually appointable on this dayž
		$end_date                    = $this->get_max_allowed_date_into_the_future( $end_date );

		#error_log( var_export( date( 'Y-n-j H:i', $check_date ) .'___'. date( 'Y-n-j H:i', $end_date ), true ) );
		#print '<pre>'; print_r( $rules ); print '</pre>';

		// Get available slot start times.
		#$minutes_not_available = $this->get_unavailable_minutes( $scheduled ); // Get unavailable slot start times.
		#print '<pre>'; print_r( $minutes_not_available ); print '</pre>';

		// Looping day by day look for available slots.
		while ( $check_date <= $end_date ) {
			#print '<pre>'; print_r( $check_date ); print '</pre>';
			$appointable_minutes_for_date = array_merge( $default_appointable_minutes, WC_Product_Appointment_Rule_Manager::get_minutes_from_rules( $rules, $check_date ) );

			#error_log( var_export( $appointable_minutes_for_date, true ) );
			// Run through rules only when minutes are not empty.
			if ( [] !== $appointable_minutes_for_date ) {
				if ( ! $this->get_default_availability() ) {
					// From an array of minutes for a day, remove all minutes before first slot time.
					$appointable_minutes_for_date = array_filter(
					    $appointable_minutes_for_date,
					    fn($minute): bool => $first_slot_time_minute <= $minute,
					);
				}
				#print '<pre>'; print_r( $appointable_minutes_for_date ); print '</pre>';
				$appointable_start_and_end = $this->get_appointable_minute_start_and_end( $appointable_minutes_for_date );
				#print '<pre>'; print_r( $appointable_start_and_end ); print '</pre>';
				$slots = $this->get_appointable_minute_slots_for_date( $check_date, $start_date, $end_date, $appointable_start_and_end, $intervals, $staff_id, $minutes_not_available, $get_past_times );
				#print '<pre>'; print_r( $slots ); print '</pre>';
				$slot_start_times_in_range = array_merge( $slots, $slot_start_times_in_range );
				#print '<pre>'; print_r( $slot_start_times_in_range ); print '</pre>';
			}

			$check_date = strtotime( '+1 day', $check_date ); // Move to the next day
		}

		return $slot_start_times_in_range;
	}

	/**
	 * @param array $appointable_minutes
	 *
	 * @return array
	 */
	public function get_appointable_minute_start_and_end( array $appointable_minutes ): array {
		// Break appointable minutes into sequences - appointments cannot have breaks
		$appointable_minute_slots     = [];
		$appointable_minute_slot_from = current( $appointable_minutes );

		foreach ( $appointable_minutes as $key => $minute ) {
			if ( isset( $appointable_minutes[ $key + 1 ] ) ) {
				if ( $appointable_minutes[ $key + 1 ] - 1 === $minute ) {
					continue;
				}
                // There was a break in the sequence
                $appointable_minute_slots[]   = [ $appointable_minute_slot_from, $minute + 1 ];
                $appointable_minute_slot_from = $appointable_minutes[ $key + 1 ];
			} else {
				// We're at the end of the appointable minutes
				$appointable_minute_slots[] = [ $appointable_minute_slot_from, $minute + 1 ];
			}
		}

		/*
		// Find slots that don't span any amount of time (same start + end)
		foreach ( $appointable_minute_slots as $key => $appointable_minute_slot ) {
			if ( $appointable_minute_slot[0] === $appointable_minute_slot[1] ) {
				$keys_to_remove[] = $key; // track which slots need removed
			}
		}
		// Remove all of our slots
		if ( ! empty( $keys_to_remove ) ) {
			foreach ( $keys_to_remove as $key ) {
				unset( $appointable_minute_slots[ $key ] );
			}
		}
		*/

		return $appointable_minute_slots;
	}

	/**
	 * Return an array of that is not available for appointment.
	 *
	 * @since 2.3.0 introduced.
	 * @since 4.10.5 disabled as setting unavailable minutes should only apply with no staff.
	 *
	 * @param array $scheduled. Pairs of scheduled slot start and end times.
	 * @return array $scheduled_minutes
	 */
	public function get_unavailable_minutes( array $scheduled ): array {
		$minutes_not_available = [];
		$padding               = ( $this->get_padding_duration_in_minutes() ?: 0 ) * 60;
		#print '<pre>'; print_r( $padding ); print '</pre>';
		foreach ( $scheduled as $scheduled_slot ) {
			$start = $scheduled_slot[0] - $padding;
			$end   = $scheduled_slot[1] + $padding;
			for ( $i = $start; $i < $end; $i += 60 ) {
				$minutes_not_available[] = $i; #previously set as: array_push( $minutes_not_available, $i );
			}
		}

		return array_count_values( $minutes_not_available );
	}

	/**
	 * Returns slots/time slots from a given start and end minute slots.
	 *
	 * This function take varied inputs but always retruns a slot array of available slots.
	 * Sometimes it gets the minutes and see if all is available some times it needs to make up the
	 * minutes based on what is scheduled.
	 *
	 * It uses start and end date to figure things out.
	 *
	 * @since 2.3.0 introduced.
	 *
	 * @param $check_date
	 * @param $start_date
	 * @param $end_date
	 * @param $appointable_start_and_end
	 * @param $intervals
	 * @param $staff_id
	 * @param $minutes_not_available
	 * @param $get_past_times
	 *
	 * @return array
	 */
	public function get_appointable_minute_slots_for_date( $check_date, $start_date, $end_date, $appointable_start_and_end, $intervals, $staff_id, $minutes_not_available, $get_past_times ) {
		// slots as in an array of slots. $slot_start_times
		$slots = [];

		// boring interval stuff
		$interval      = $intervals[0]; #duration
		$base_interval = $intervals[1]; #interval

		// get a time stamp to check from and get a time stamp to check to
		$product_min_date = $this->get_min_date_a();
		$product_max_date = $this->get_max_date_a();

		#print '<pre>'; print_r( $product_min_date ); print '</pre>';
		#print '<pre>'; print_r( $product_max_date ); print '</pre>';

		$min_check_from = strtotime( "+{$product_min_date['value']} {$product_min_date['unit']}", current_time( 'timestamp' ) );
		$max_check_to   = strtotime( "+{$product_max_date['value']} {$product_max_date['unit']}", current_time( 'timestamp' ) );
		$min_date       = wc_appointments_get_min_timestamp_for_day( $start_date, $product_min_date['value'], $product_min_date['unit'] );

		#print '<pre>'; print_r( date( 'Y-m-d H:i', $min_check_from ) ); print '</pre>';
		#print '<pre>'; print_r( date( 'Y-m-d H:i', $max_check_to ) ); print '</pre>';
		#print '<pre>'; print_r( date( 'Y-m-d H:i', $min_date ) ); print '</pre>';

		$current_time_stamp = current_time( 'timestamp' );

		// if we have a padding, we will shift all times accordingly by changing the from_interval
		// e.g. 60 min paddingpadding shifts [ 480, 600, 720 ] into [ 480, 660, 840 ]
		#$padding = $this->get_padding_duration_in_minutes() ?: 0;

		#print '<pre>'; print_r( $check_date ); print '</pre>';
		#print '<pre>'; print_r( $appointable_start_and_end ); print '</pre>';
		#print '<pre>'; print_r( $minutes_not_available ); print '</pre>';

		// Loop the slots of appointable minutes and add a slot if there is enough room to book
		foreach ( $appointable_start_and_end as $time_slot ) {
			$range_start = $time_slot[0];
			$range_end   = $time_slot[1];

			/*
			// Adding 1 minute to round up to a full hour.
			if ( 'hour' === $this->get_duration_unit() ) {
				$range_end  += 1;
			}
			*/

			/*
			$time_slot_start        = strtotime( "midnight +{$range_start} minutes", $check_date );
			$minutes_in_slot        = $range_end - $range_start;
			$base_intervals_in_slot = floor( $minutes_in_slot / $base_interval );
			$time_slot_end_time 	= strtotime( "midnight +{$range_end} minutes", $check_date );
			*/

			$range_start_time       = strtotime( "midnight +{$range_start} minutes", $check_date );
            $range_end_time         = strtotime( "midnight +{$range_end} minutes", $check_date );
            $minutes_for_range      = $range_end - $range_start;
            // Compute slot start count: floor((L - D)/S) + 1 when L >= D.
            // L = $minutes_for_range, D = $interval (slot duration), S = $base_interval (step between starts).
            #$base_intervals_in_slot = 0; // needs testing.
			$base_intervals_in_slot = floor( $minutes_for_range / $base_interval );
            if ( $minutes_for_range >= $interval ) {
                $base_intervals_in_slot = floor( ( $minutes_for_range - $interval ) / $base_interval ) + 1;
            }

			// Only need to check first hour.
			if ( 'start' === $this->get_availability_span() ) {
				$base_interval          = 1; #test
				$base_intervals_in_slot = 1; #test
			}

			for ( $i = 0; $i < $base_intervals_in_slot; $i++ ) {
				#$from_interval = $i * ( $base_interval + $padding );
				$from_interval = $i * $base_interval;
				$to_interval   = $from_interval + $interval;
				$start_time    = strtotime( "+{$from_interval} minutes", $range_start_time );
				$end_time      = strtotime( "+{$to_interval} minutes", $range_start_time );

				#print '<pre>'; print_r( '$stime: ' . date('Y-n-j H:i', $start_time) ); print '</pre>';
				#print '<pre>'; print_r( '$etime: ' . date('Y-n-j H:i', $end_time) ); print '</pre>';

				// Remove 00:00 or 24:00 for same day slot.
				#if ( strtotime( 'midnight +1 day', $start_date ) === $start_time ) {
					#continue;
				#}

				// Available quantity.
				$available_qty = WC_Product_Appointment_Rule_Manager::check_availability_rules_against_time( $this, $start_time, $end_time, $staff_id, true );
                #$available_qty = $this->get_available_qty( $staff_id ); // exact quantity is checked in get_available_slots_html() function
                #print '<pre>'; print_r( date('Y-n-j H:i', $check_date) . '......' . date( 'Y-n-j H:i', $start_time ) . '.' . date( 'Y-n-j H:i', $end_time ) . '___' . $available_qty . '___' . $staff_id ); print '</pre>';
                // Staff must be available or skip if no staff and no availability.
                if (! $available_qty && $staff_id) {
                    continue;
                }
                if (! $this->has_staff() && ! $available_qty) {
                    continue;
                }

				// Break if start time is after the end date being calculated.
				if ( $start_time > $end_date && ( 'start' !== $this->get_availability_span() ) ) {
					break 2;
				}

				#print '<pre>'; print_r( date( 'Y-m-d H:i', $start_time ) .' < '. date( 'Y-m-d H:i', $min_check_from ) ); print '</pre>';

				// Must be in the future.
				if ( ( $start_time < $min_date || $start_time <= $current_time_stamp ) && ! $get_past_times ) {
					continue;
				}

				/*
				// Disabled with 4.10.0.
				// Skip if start minutes not available.
				if ( isset( $minutes_not_available[ $start_time ] )
				    && ! $this->is_staff_assignment_type( 'all' )
				    && $minutes_not_available[ $start_time ] >= $available_qty ) {
					continue;
				}

				// Skip if any minute is not available.
				// Not when checking availability against starting slot only.
				if ( 'start' !== $this->get_availability_span()
			        && ! $this->is_staff_assignment_type( 'all' ) ) {
					$interval_not_appointable = false;
					// Check if any minute of slot is not within not available minutes.
					for ( $t = $start_time; $t < $end_time; $t += 60 ) {
						if ( isset( $minutes_not_available[ $t ] ) && $minutes_not_available[ $t ] >= $available_qty ) {
							$interval_not_appointable = true;
							break;
						}
					}

					if ( $interval_not_appointable ) {
						continue;
					}
				}
				*/

				// Make sure minute & hour slots are not past minimum & max appointment settings.
				if ( ( $start_time < $min_check_from || $end_time < $min_check_from || $start_time > $max_check_to ) && ! $get_past_times ) {
					continue;
				}

				// Make sure slot doesn't start after the end date.
				if ( $start_time > $end_date ) {
					continue;
				}

				// Block slots that end beyond the appointable range end when span is 'all'.
				// This prevents late-day starts (e.g., 18:30 for a 120-min slot) from exceeding a 20:00 end.
				if ( 'start' !== $this->get_availability_span() && $end_time > $range_end_time ) {
					continue;
				}

				if ( ! in_array( $start_time, $slots ) ) {
					$slots[] = $start_time;
				}
			}
		}

		#var_dump( $slots );

		return $slots;
	}

	/**
	 * Returns available slots from a range of slots by looking at existing appointments.
	 *
	 * @param  array $args
	 *     @option  array   $slots                 The slots we'll be checking availability for.
	 *     @option  array   $intervals             Array containing 2 items; the interval of the slot (maybe user set), and the base interval for the slot/product.
	 *     @option  integer $staff_id              Resource we're getting slots for. Falls backs to product as a whole if 0.
	 *     @option  integer $from                  The starting date for the set of slots
	 *     @option  integer $to                    Ending date for the set of slots
	 *     @option  array   $existing_appointments List of existing appointments
	 *
	 * @return array The available slots array
	 */
	public function get_available_slots( $args ) {
		$args = wp_parse_args(
		    $args,
		    [
				'slots'        => [],
				'intervals'    => [],
				'staff_id'     => 0,
				'from_range'   => '',
				'to_range'     => '',
				'from'         => '',
				'to'           => '',
				'appointments' => 0,
			],
		);

		$slots                 = $args['slots'];
		$intervals             = $args['intervals'];
		$staff_id              = $args['staff_id'];
		$from                  = $args['from'];
		$to                    = $args['to'];
		$existing_appointments = $args['appointments'];

		// Check if we can use indexed availability within horizon
		$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 && ( $to <= $horizon_ts );
		}

		// Use indexed availability if enabled and within horizon
		if ( $use_indexed && class_exists( 'WC_Appointments_Controller' ) && method_exists( 'WC_Appointments_Controller', 'get_cached_available_slots' ) ) {
			return \WC_Appointments_Controller::get_cached_available_slots(
			    [
					'product'      => $this,
					'slots'        => $slots,
					'intervals'    => $intervals,
					'staff_id'     => $staff_id,
					'from'         => $from,
					'to'           => $to,
				],
			);
		}

		// Appointments not defined. Run a query.
		if ( 0 === $existing_appointments ) {
			$existing_appointments = WC_Appointment_Data_Store::get_all_existing_appointments( $this, $from, $to, $staff_id );
		}

		// Fall back to original method when horizon exceeded or indexing disabled
		$intervals = empty( $intervals ) ? $this->get_intervals() : $intervals;

 		[$interval, $base_interval] = $intervals;

 		$available_times = [];

		$start_date = $from;
		if ( empty( $start_date ) ) {
			$start_date = reset( $slots );
		}

		$end_date = $to;
		if ( empty( $end_date ) ) {
			$end_date = absint( end( $slots ) );
		}

		$product_staff = $this->has_staff() && ! $staff_id ? $this->get_staff_ids() : $staff_id;

		$original_available_qty = $this->get_available_qty( $staff_id );
		#print '<pre>'; print_r( $original_available_qty ); print '</pre>';

 		if ( ! empty( $slots ) ) {

 			// Staff scheduled array. Staff can be a "staff" but also just an appointment if it has no staff.
 			$staff_scheduled   = [
				0 => [],
			];
 			$product_scheduled = [
				0 => [],
			];

			if ( ! empty( $existing_appointments ) ) {
				foreach ( $existing_appointments as $existing_appointment ) {
					$appointment_staff_ids  = $existing_appointment->get_staff_ids();
	 				$appointment_product_id = $existing_appointment->get_product_id();
                     // Staff doesn't match, so don't check.
                     if ($product_staff
 						&& ! is_array( $product_staff )
 					    && $appointment_staff_ids
 					 	&& is_array( $appointment_staff_ids )
 					 	&& ! in_array( $product_staff, $appointment_staff_ids )) {
                         continue;
                     }

					// Staff doesn't match, so don't check.
					if ($product_staff
						&& is_array( $product_staff )
					    && $appointment_staff_ids
						&& is_array( $appointment_staff_ids )
						&& ! array_intersect( $product_staff, $appointment_staff_ids )) {
                        continue;
                    }

	 				// Prepare staff and product array.
	 				foreach ( (array) $appointment_staff_ids as $appointment_staff_id ) {
	 					$staff_scheduled[ $appointment_staff_id ] ??= [];
	 				}
	 				$product_scheduled[ $appointment_product_id ] ??= [];

	 				// Slot start/end time.
					$start_time = $existing_appointment->get_start();
					$end_time   = $existing_appointment->get_end();

	 				// Existing appointment lasts all day, force end day time.
	 				if ( $existing_appointment->is_all_day() && in_array( $this->get_duration_unit(), [ 'minute', 'hour' ] ) ) {
	 					$end_time = strtotime( 'midnight +1 day', $end_time );
	 				}

	 				// Product duration set to day, force daily check
	 				if ( 'day' === $this->get_duration_unit() ) {
	 					$start_time = strtotime( 'midnight', $start_time );
	 					$end_time   = strtotime( 'midnight +1 day', $end_time );
	 				}

	 				// When existing appointment is scheduled with another product,
	 				// remove all available capacity, so staff becomes unavailable for this product.
	 				if ( $this->get_id() !== $appointment_product_id ) {
						$check = apply_filters( 'wc_appointments_check_appointment_product', true, $existing_appointment->get_id(), $this->get_id() );
					    $check = apply_filters( 'wc_apointments_check_appointment_product', true, $existing_appointment->get_id(), $this->get_id() ); // BC for old hook name
						$repeat = $check ? max( 1, $original_available_qty ) : max( 1, $existing_appointment->get_qty() );
	 				// Only remove capacity scheduled for existing product.
	 				} else {
	 					$repeat = max( 1, $existing_appointment->get_qty() );
	 				}
					$repeat = apply_filters( 'wc_appointments_check_appointment_qty', $repeat, $existing_appointment, $this );

	 				// Repeat to add capacity for each scheduled qty.
	 				foreach ( (array) $appointment_staff_ids as $appointment_staff_id ) {
	 					for ( $i = 0; $i < $repeat; $i++ ) {
	 						$staff_scheduled[ $appointment_staff_id ][] = [ $start_time, $end_time ];
	 					}
	 				}
	 				for ( $i = 0; $i < $repeat; $i++ ) {
	 					$product_scheduled[ $appointment_product_id ][] = [ $start_time, $end_time ];
	 				}
	 			}
			}

			// Available times for product: Generate arrays that contain information about what slots to unset.
	        #print '<pre>'; print_r( 'TEST' ); print '</pre>';
	        $available_times = array_merge(
	            $available_times,
	            $this->get_slots_in_range( $start_date, $end_date, [ $interval, $base_interval ], $product_staff ),
	        );

			/*
 			// Test
 			$test = [];
 			foreach ( $available_times as $available_time ) {
 				$test[] = date( 'Y-m-d H:i', $available_time );
 			}
 			print '<pre>'; print_r( $test ); print '</pre>';
			*/

 			if ( $this->has_staff() ) {

				// Build staff times array.
				$staff_times = [];

				// Loop through all staff in array.
				if ( is_array( $product_staff ) ) {
					foreach ( $product_staff as $product_staff_id ) {
						$staff_appointments = $staff_scheduled[ $product_staff_id ] ?? [];
                        $staff_times_a      = $this->get_slots_in_range( $start_date, $end_date, [ $interval, $base_interval ], $product_staff_id, $staff_appointments );
						#$staff_times_a      = $this->get_slots_in_range( $start_date, $end_date, [ $interval, $base_interval ], $product_staff_id );
						#print '<pre>'; print_r( $staff_times_a ); print '</pre>';
						$staff_times = array_merge( $staff_times_a, $staff_times );
					}
					#print '<pre>'; print_r( $staff_times ); print '</pre>';

				// Single staff.
				} elseif ( isset( $staff_scheduled[ $staff_id ] ) && (isset($staff_scheduled[ $staff_id ]) && [] !== $staff_scheduled[ $staff_id ]) ) {
					$staff_appointments = $staff_scheduled[ $staff_id ] ?? [];
                    $staff_times        = $this->get_slots_in_range( $start_date, $end_date, [ $interval, $base_interval ], $staff_id, $staff_appointments );
					#print '<pre>'; print_r( $staff_times ); print '</pre>';

				// When scheduling outside of staff scope.
				} elseif ( isset( $staff_scheduled[0] ) ) {
					$staff_appointments = $staff_scheduled[0] ?? [];
					$staff_times        = $this->get_slots_in_range( $start_date, $end_date, [ $interval, $base_interval ], $staff_id, $staff_appointments );
					#print '<pre>'; print_r( $staff_times ); print '</pre>';
				// Everything else.
				} else {
					$staff_times = $this->get_slots_in_range( $start_date, $end_date, [ $interval, $base_interval ], $staff_id, [] );
					#print '<pre>'; print_r( $staff_times ); print '</pre>';
				}

				#print '<pre>'; print_r( $staff_times ); print '</pre>';

				// No preference selected.
				if ( ! $staff_id ) {
					$available_times = array_merge( $available_times, $staff_times ); #add times from staff to times from product
				// Staff selected.
				} else {
					$available_times = array_intersect( $available_times, $staff_times ); #merge times from staff that are also available in product
				}
				#print '<pre>'; print_r( $available_times ); print '</pre>';
			}

 			/*
 			// Test
 			$test2 = [];
 			foreach ( $available_times as $available_slot ) {
 				$test2[] = date( 'y-m-d H:i', $available_slot );
 			}
			print '<pre>'; print_r( $test2 ); print '</pre>';
 			*/
 		}

		$available_times = array_unique( $available_times );
		sort( $available_times );

		/*
		// Test
		$test = [];
		foreach ( $available_times as $available_slot ) {
			$test[] = date( 'y-m-d H:i', $available_slot );
		}
		print '<pre>'; print_r( $test ); print '</pre>';
		*/

		/**
		 * Filter the available slots for a product within a given range
		 *
		 * @since 1.9.8 introduced
		 *
		 * @param array $available_times
		 * @param WC_Product $appointments_product
		 * @param array $raw_range passed into this function.
		 * @param array $intervals
		 * @param integer $staff_id
		 */
		return apply_filters( 'wc_appointments_product_get_available_slots', $available_times, $this, $slots, $intervals, $staff_id );
 	}

	/**
	 * Get the availability of all staff
	 *
	 * @param string $start_date
	 * @param string $end_date
	 * @param integer $qty
	 * @return array| WP_Error
	 */
	public function get_all_staff_availability( $start_date, $end_date, $qty ) {
		$staff_ids       = $this->get_staff_ids();
		$available_staff = [];

		foreach ( $staff_ids as $staff_id ) {
			$availability = wc_appointments_get_total_available_appointments_for_range( $this, $start_date, $end_date, $staff_id, $qty );

			if ( $availability && ! is_wp_error( $availability ) ) {
				$available_staff[ $staff_id ] = $availability;
			}
		}

		if ( [] === $available_staff ) {
			return new WP_Error( 'Error', __( 'This slot cannot be scheduled.', 'woocommerce-appointments' ) );
		}

		return $available_staff;
	}


	/*
	|--------------------------------------------------------------------------
	| Deprecated Methods
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get the minutes that should be available based on the rules and the date to check.
	 *
	 * The minutes are returned in a range from the start incrementing minutes right up to the last available minute.
	 *
	 * @deprecated since 2.6.5
	 * @param array $rules
	 * @param int $check_date
	 * @return array $appointable_minutes
	 */
	public function get_minutes_from_rules( $rules, $check_date ) {
		return WC_Product_Appointment_Rule_Manager::get_minutes_from_rules( $rules, $check_date );
	}

	/**
	 * Find the minimum slot's timestamp based on settings.
	 *
	 * @deprecated Replaced with wc_appointments_get_min_timestamp_for_day
	 * @return int
	 */
	public function get_min_timestamp_for_date( $start_date, $product_min_date_value, $product_min_date_unit ) {
		return wc_appointments_get_min_timestamp_for_day( $start_date, $product_min_date_value, $product_min_date_unit );
	}

	/**
	 * Sort rules.
	 *
	 * @deprecated Replaced with WC_Product_Appointment_Rule_Manager::sort_rules_callback
	 */
	public function rule_override_power_sort( $rule1, $rule2 ) {
		return WC_Product_Appointment_Rule_Manager::sort_rules_callback( $rule1, $rule2 );
	}

	/**
	 * Return an array of staff which can be scheduled for a defined start/end date
	 *
	 * @deprecated Replaced with wc_appointments_get_slot_availability_for_range
	 * @param  string $start_date
	 * @param  string $end_date
	 * @param  string $staff_id
	 * @param  integer $qty being scheduled
	 * @return bool|WP_ERROR if no slots available, or int count of appointments that can be made, or array of available staff
	 */
	public function get_available_appointments( $start_date, $end_date, $staff_id = '', $qty = 1 ) {
		return wc_appointments_get_total_available_appointments_for_range( $this, $start_date, $end_date, $staff_id, $qty );
	}

	/**
	 * Get existing appointments in a given date range
	 *
	 * @param string $start_date
	 * @param string $end_date
	 * @param int    $staff_id
	 * @return array
	 */
	public function get_appointments_in_date_range( $start_date, $end_date, $staff_id = null ) {
		if ( $this->has_staff() && $staff_id ) {
			if ( ! is_array( $staff_id ) ) {
				$staff_id = [ $staff_id ];
			}
		} elseif ( $this->has_staff() && ! $staff_id ) {
			$staff_id = $this->get_staff_ids();
		}

		return WC_Appointment_Data_Store::get_all_existing_appointments( $this, $start_date, $end_date, $staff_id );
		#return WC_Appointment_Data_Store::get_appointments_in_date_range( $start_date, $end_date, $this->get_id(), $staff_id );
	}

	/**
	 * Check a date against the availability rules
	 *
	 * @param  string $check_date date to check
	 * @return bool available or not
	 */
	public function check_availability_rules_against_date( $check_date, $staff_id, $get_capacity = false ) {
		// Check if we can use indexed availability within horizon
		$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 && ( $check_date <= $horizon_ts );
		}

		// Use indexed availability if enabled and within horizon
		if ( $use_indexed && class_exists( 'WC_Appointments_Controller' ) && method_exists( 'WC_Appointments_Controller', 'check_cached_availability_rules_against_date' ) ) {
			return \WC_Appointments_Controller::check_cached_availability_rules_against_date( $this, $check_date, $staff_id, $get_capacity );
		}

		// Fall back to original method when horizon exceeded or indexing disabled
		return WC_Product_Appointment_Rule_Manager::check_availability_rules_against_date( $this, $check_date, $staff_id, $get_capacity );
	}

	/**
	 * Check a time against the time specific availability rules
	 *
	 * @param  string  $slot_start_time timestamp to check
	 * @param  string  $slot_end_time   timestamp to check
	 * @return bool    available or not
	 */
	public function check_availability_rules_against_time( $slot_start_time, $slot_end_time, $staff_id, $get_capacity = false ) {
		// Check if we can use indexed availability within horizon
		$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 && ( $slot_end_time <= $horizon_ts );
		}

		// Use indexed availability if enabled and within horizon
		if ( $use_indexed && class_exists( 'WC_Appointments_Controller' ) && method_exists( 'WC_Appointments_Controller', 'check_cached_availability_rules_against_time' ) ) {
			return \WC_Appointments_Controller::check_cached_availability_rules_against_time( $this, $slot_start_time, $slot_end_time, $staff_id, $get_capacity );
		}

		// Fall back to original method when horizon exceeded or indexing disabled
		return WC_Product_Appointment_Rule_Manager::check_availability_rules_against_time( $this, $slot_start_time, $slot_end_time, $staff_id, $get_capacity );
	}

	/**
	 * Checks appointment data is correctly set, and that the chosen slots are indeed available.
	 *
	 * @since 4.7.0
	 *
	 * @param  array $data
	 *
	 * @return bool|WP_Error on failure, true on success
	 */
	public function is_appointable( array $data ) {
		// Validate staff are set
		if ( $this->has_staff() && $this->is_staff_assignment_type( 'customer' ) ) {
			if ( empty( $data['_staff_id'] ) ) {
				// return new WP_Error( 'Error', sprintf( __( 'Please choose the %s.', 'woocommerce-appointments' ), $this->get_staff_label() ? $this->get_staff_label() : __( 'Providers', 'woocommerce-appointments' ) ) );
				$data['_staff_id'] = 0;
			}
		} elseif ( $this->has_staff() && $this->is_staff_assignment_type( 'automatic' ) ) {
			$data['_staff_id'] = 0;
		} elseif ( $this->has_staff() && $this->is_staff_assignment_type( 'all' ) ) {
			$data['_staff_id'] = 0;
		} else {
			$data['_staff_id'] = '';
		}

		// Validate date and time
		if ( empty( $data['date'] ) ) {
			return new WP_Error( 'Error', __( 'Date is required - please choose one above', 'woocommerce-appointments' ) );
		}
		if ( in_array( $this->get_duration_unit(), [ self::DURATION_UNIT_MINUTE, self::DURATION_UNIT_HOUR ], true ) && empty( $data['time'] ) ) {
			return new WP_Error( 'Error', __( 'Time is required - please choose one above', 'woocommerce-appointments' ) );
		}
		if ( $data['_date'] && date( 'Ymd', strtotime( $data['_date'] ) ) < date( 'Ymd', current_time( 'timestamp' ) ) ) {
			return new WP_Error( 'Error', __( 'You must choose a future date and time.', 'woocommerce-appointments' ) );
		}
		if ( $data['_date'] && ! empty( $data['_time'] ) && date( 'YmdHi', strtotime( $data['_date'] . ' ' . $data['_time'] ) ) < date( 'YmdHi', current_time( 'timestamp' ) ) ) {
			return new WP_Error( 'Error', __( 'You must choose a future date and time.', 'woocommerce-appointments' ) );
		}

		// Enforce configured quantity bounds before any availability lookups.
		$requested_qty = max( 1, (int) ( $data['_qty'] ?? 1 ) );
		$min_qty       = max( 1, (int) $this->get_qty_min() );
		$max_qty       = (int) $this->get_qty_max();

		if ( $requested_qty < $min_qty ) {
			/* translators: %d: minimum quantity */
			return new WP_Error( 'Error', sprintf( __( 'Please choose at least %d for this appointment.', 'woocommerce-appointments' ), $min_qty ) );
		}

		if ( 0 < $max_qty && $requested_qty > $max_qty ) {
			/* translators: %d: maximum quantity */
			return new WP_Error( 'Error', sprintf( __( 'Please choose %d or fewer for this appointment.', 'woocommerce-appointments' ), $max_qty ) );
		}

		// Validate min date and max date
		if ( in_array( $this->get_duration_unit(), [ self::DURATION_UNIT_MINUTE, self::DURATION_UNIT_HOUR ], true ) ) {
			$now = current_time( 'timestamp' );
		} elseif ( self::DURATION_UNIT_MONTH === $this->get_duration_unit() ) {
			$now = strtotime( 'midnight first day of this month', current_time( 'timestamp' ) );
		} else {
			$now = strtotime( 'midnight', current_time( 'timestamp' ) );
		}

		$min = $this->get_min_date_a();
		if ( $min ) {
			$min_date = wc_appointments_get_min_timestamp_for_day( strtotime( $data['_date'] ), $min['value'], $min['unit'] );

			if ( strtotime( $data['_date'] . ' ' . $data['_time'] ) < $min_date ) {
				/* translators: %s: minimum date */
				return new WP_Error( 'Error', sprintf( __( 'The earliest appointment possible is currently %s.', 'woocommerce-appointments' ), date_i18n( wc_appointments_date_format() . ' ' . wc_appointments_time_format(), $min_date ) ) );
			}
		}

		$max = $this->get_max_date_a();
		if ( $max ) {
			$max_date = strtotime( "+{$max['value']} {$max['unit']}", $now );
			if ( strtotime( $data['_date'] . ' ' . $data['_time'] ) > $max_date ) {
				/* translators: %s: maximum date */
				return new WP_Error( 'Error', sprintf( __( 'The latest appointment possible is currently %s.', 'woocommerce-appointments' ), date_i18n( wc_appointments_date_format() . ' ' . wc_appointments_time_format(), $max_date ) ) );
			}
		}

		// Check that the day of the week is not restricted.
		if ( $this->has_restricted_days() ) {
			$restricted_days = (array) $this->get_restricted_days();

			if ( ! in_array( date( 'w', $data['_start_date'] ), $restricted_days ) ) {
				return new WP_Error( 'Error', __( 'Sorry, appointments cannot start on this day.', 'woocommerce-appointments' ) );
			}
		}

		// Get availability for the dates
		$available_appointments = wc_appointments_get_total_available_appointments_for_range( $this, $data['_start_date'], $data['_end_date'], $data['_staff_id'], $data['_qty'] );
		#error_log( 'Available appointments: ' . print_r( $available_appointments, true ) );;

		if ( is_array( $available_appointments ) ) {
			$this->assigned_staff_id = current( array_keys( $available_appointments ) );
		}
        if (is_wp_error( $available_appointments )) {
            return $available_appointments;
        }

		if (! $available_appointments) {
            return new WP_Error( 'Error', __( 'Sorry, the selected slot is not available.', 'woocommerce-appointments' ) );
        }

		return true;
	}

	/**
	 * Compares the provided date with the appointment max allowed date and returns
	 * the earliest.
	 *
	 * @since 4.8.14
	 * @param  int $end_date Date to compare.
	 * @return int
	 */
	private function get_max_allowed_date_into_the_future( $end_date ) {
		$product_max_date = $this->get_max_date();

		if ( $product_max_date ) {
			return $end_date;
		}

		$max_end_date = strtotime( "+{$product_max_date['value']} {$product_max_date['unit']}", current_time( 'timestamp' ) );

		return $end_date > $max_end_date ? $max_end_date : $end_date;
	}

	/**
	 * Calculate staff availability for a specific slot.
	 *
	 * @param int   $slot Slot timestamp.
	 * @param int   $interval Interval in minutes.
	 * @param mixed $product_staff Product staff (int, array, or 0).
	 * @param bool  $product_has_staff Whether product has staff.
	 * @param string $product_duration_unit Duration unit.
	 * @param int   $product_available_qty Default available quantity.
	 * @return array [staff array, available_qty, max_available_qty]
	 */
	private function calculate_staff_availability_for_slot( int $slot, int $interval, $product_staff, bool $product_has_staff, string $product_duration_unit, int $product_available_qty ): array {
		$staff         = [ 0 => $product_available_qty ];
		$available_qty = 0;
		$max_available_qty = 0;

		if ( $product_has_staff && $product_staff && is_array( $product_staff ) ) {
			$staff_qtys = [];
			foreach ( $product_staff as $product_staff_id ) {
				// Only include if it is available for this selection.
				if ( ! WC_Product_Appointment_Rule_Manager::check_availability_rules_against_date( $this, $slot, $product_staff_id ) ) {
					$staff[ $product_staff_id ] = 0;
					continue;
				}

				// Get qty based on duration unit.
				if ( in_array( $product_duration_unit, [ self::DURATION_UNIT_MINUTE, self::DURATION_UNIT_HOUR ], true ) ) {
					$check_rules_against_time = WC_Product_Appointment_Rule_Manager::check_availability_rules_against_time( $this, $slot, strtotime( "+{$interval} minutes", $slot ), $product_staff_id, true );

					if ( ! $check_rules_against_time || 0 === $check_rules_against_time ) {
						$staff[ $product_staff_id ] = 0;
						continue;
					}

					$get_available_qty = $check_rules_against_time;
				} else {
					$get_available_qty = WC_Product_Appointment_Rule_Manager::check_availability_rules_against_date( $this, $slot, $product_staff_id, true );
				}

				$staff_qtys[] = $get_available_qty;
				$staff[ $product_staff_id ] = $get_available_qty;
				$available_qty += $get_available_qty;
			}

			$max_available_qty = is_array( $staff_qtys ) && [] !== $staff_qtys ? max( $staff_qtys ) : 0;

		} elseif ( $product_has_staff && $product_staff && is_int( $product_staff ) ) {
			// Get qty based on duration unit.
			$check_rules_against_time = WC_Product_Appointment_Rule_Manager::check_availability_rules_against_time( $this, $slot, strtotime( "+{$interval} minutes", $slot ), $product_staff, true );
			if ( ! $check_rules_against_time || 0 === $check_rules_against_time ) {
				$staff[ $product_staff ] = 0;
				return [ $staff, 0, 0 ];
			}

			// Get qty based on duration unit.
			if ( in_array( $product_duration_unit, [ self::DURATION_UNIT_MINUTE, self::DURATION_UNIT_HOUR ], true ) ) {
				$get_available_qty = $check_rules_against_time;
			} else {
				$get_available_qty = WC_Product_Appointment_Rule_Manager::check_availability_rules_against_date( $this, $slot, $product_staff, true );
			}

			$staff[ $product_staff ] = $get_available_qty;
			$available_qty += $get_available_qty;
			$max_available_qty = $get_available_qty;
		} else {
			// Get qty based on duration unit.
			if ( in_array( $product_duration_unit, [ self::DURATION_UNIT_MINUTE, self::DURATION_UNIT_HOUR ], true ) ) {
				$get_available_qty = WC_Product_Appointment_Rule_Manager::check_availability_rules_against_time( $this, $slot, strtotime( "+{$interval} minutes", $slot ), $product_staff, true );
			} else {
				$get_available_qty = WC_Product_Appointment_Rule_Manager::check_availability_rules_against_date( $this, $slot, $product_staff, true );
			}

			$staff[0] = $get_available_qty;
			$available_qty += $get_available_qty;
			$max_available_qty = $get_available_qty;
		}

		return [ $staff, $available_qty, $max_available_qty ];
	}

	/**
	 * Build day-level booked quantities for per-day quantity basis.
	 *
	 * @param array $existing_appointments Existing appointments.
	 * @param int   $product_id Product ID.
	 * @param int   $padding_duration_in_minutes Padding duration in minutes.
	 * @param array $appointment_padding_cache Cache for appointment padding lookups.
	 * @return array Day booked quantities by staff.
	 */
	private function build_day_booked_quantities( array $existing_appointments, int $product_id, int $padding_duration_in_minutes, array &$appointment_padding_cache ): array {
		$day_booked_by_staff = [];

		foreach ( $existing_appointments as $existing_appointment ) {
			$appointment_staff_ids = wc_appointments_normalize_staff_ids( $existing_appointment->get_staff_ids() );
			$appointment_staff_ids = [] === $appointment_staff_ids ? [ 0 ] : $appointment_staff_ids;

			$appointment_qty = $existing_appointment->get_qty() ? (int) $existing_appointment->get_qty() : 1;

			$combined_padding_minutes = (int) ( $padding_duration_in_minutes ?: 0 );
			$appointment_product_id   = $existing_appointment->get_product_id();

			if ( $appointment_product_id && $appointment_product_id !== $product_id ) {
				if ( ! array_key_exists( $appointment_product_id, $appointment_padding_cache ) ) {
					$other_product = $existing_appointment->get_product();
					$appointment_padding_cache[ $appointment_product_id ] = ( $other_product && method_exists( $other_product, 'get_padding_duration_in_minutes' ) ) ? (int) $other_product->get_padding_duration_in_minutes() : 0;
				}
				$combined_padding_minutes += $appointment_padding_cache[ $appointment_product_id ];
			}

			$padded_start = $existing_appointment->get_start();
			$padded_end   = $existing_appointment->get_end();

			if ( 0 < $combined_padding_minutes ) {
				if ( ! empty( $padded_start ) ) {
					$padded_start = strtotime( "-{$combined_padding_minutes} minutes", $padded_start );
				}
				if ( ! empty( $padded_end ) ) {
					$padded_end = strtotime( "+{$combined_padding_minutes} minutes", $padded_end );
				}
			}
			if ( empty( $padded_start ) ) {
				continue;
			}
			if ( empty( $padded_end ) ) {
				continue;
			}

			$span_end = max( $padded_end - 1, $padded_start );

			for ( $day = strtotime( 'midnight', $padded_start ); strtotime( 'midnight', $span_end ) >= $day; $day = strtotime( '+1 day', $day ) ) {
				foreach ( $appointment_staff_ids as $appointment_staff_id ) {
					$staff_key = (int) $appointment_staff_id;

					if ( ! isset( $day_booked_by_staff[ $staff_key ][ $day ] ) ) {
						$day_booked_by_staff[ $staff_key ][ $day ] = 0;
					}

					$day_booked_by_staff[ $staff_key ][ $day ] += $appointment_qty;

					// Track general bucket for products with no staff selection.
					if ( 0 !== $staff_key ) {
						if ( ! isset( $day_booked_by_staff[0][ $day ] ) ) {
							$day_booked_by_staff[0][ $day ] = 0;
						}
						$day_booked_by_staff[0][ $day ] += $appointment_qty;
					}
				}
			}
		}

		return $day_booked_by_staff;
	}

	/**
	 * Find available and scheduled slots for specific staff (if any) and return them as array.
	 *
	 * @param  array $args
	 *     @option  array   $slots          The slots we'll be checking availability for.
	 *     @option  array   $intervals      Array containing 2 items; the interval of the slot (maybe user set), and the base interval for the slot/product.
	 *     @option  integer $staff_id       Resource we're getting slots for. Falls backs to product as a whole if 0.
	 *     @option  integer $time_to_check  Specific time checking.
	 *     @option  integer $from           The starting date for the set of slots.
	 *     @option  integer $to             Ending date for the set of slots.
	 *     @option  string  $timezone       Timezone string.
	 *
	 * @return array
	 *
	 * @version  1.10.5
	 */
	public function get_time_slots( $args ) {
		$args = apply_filters(
		    'woocommerce_appointments_time_slots_args',
		    wp_parse_args(
		        $args,
		        [
					'slots'            => [],
					'intervals'        => [],
					'staff_id'         => 0,
					'time_to_check'    => 0,
					'from'             => 0,
					'to'               => 0,
					'timezone'         => 'UTC',
					'include_sold_out' => false,
				],
		    ),
		    $this,
		);

		$slots            = $args['slots'];
		$intervals        = $args['intervals'];
		$staff_id         = $args['staff_id'];
		$time_to_check    = $args['time_to_check'];
		$from             = $args['from'];
		$to               = $args['to'];
		$timezone         = $args['timezone'];
		$include_sold_out = $args['include_sold_out'];

		$intervals = empty( $intervals ) ? $this->get_intervals() : $intervals;

		[$interval, $base_interval] = $intervals;
		$interval                         = 'start' === $this->get_availability_span() ? $base_interval : $interval;

		$product_staff = $staff_id ?: 0;
		$product_staff = $this->has_staff() && ! $product_staff ? $this->get_staff_ids() : $product_staff;

		sort( $slots );

		#print '<pre>'; print_r( $slots ); print '</pre>';

		// Get appointments.
		$existing_appointments = WC_Appointment_Data_Store::get_all_existing_appointments( $this, $from, $to, $staff_id );

		// List only available slots.
		if ( ! $include_sold_out ) {
			// Fall back to original method when horizon exceeded or indexing disabled
			$slots = $this->get_available_slots(
			    [
					'slots'        => $slots,
					'intervals'    => $intervals,
					'staff_id'     => $staff_id,
					'from'         => $from,
					'to'           => $to,
					'appointments' => $existing_appointments,
				],
			);
		}

		#print '<pre>'; print_r( $slots ); print '</pre>';

		if ( empty( $slots ) ) {
			return [];
		}

		$available_slots                 = [];
		$product_available_qty           = $this->get_available_qty();
		$product_has_staff               = $this->has_staff();
		$product_duration_unit           = $this->get_duration_unit();
		$padding_duration_in_minutes     = $this->get_padding_duration_in_minutes();
		$product_is_staff_assignment_all = $this->is_staff_assignment_type( 'all' );
		$product_id                      = $this->get_id();
		$product_is_qty_per_day          = $this->is_qty_per_day();
		$day_booked_by_staff             = [];
		$day_base_capacity_by_staff      = [];
		// Cache per-appointment product padding to avoid repeated lookups.
		$appointment_padding_cache        = [];

		// Build day-level booked quantities when per-day basis is enabled.
		if ( $product_is_qty_per_day && $existing_appointments ) {
			$day_booked_by_staff = $this->build_day_booked_quantities(
				$existing_appointments,
				$product_id,
				$padding_duration_in_minutes,
				$appointment_padding_cache
			);
		}

		foreach ( $slots as $slot ) {
			$staff         = [];
			$available_qty = 0;

			// Make sure default staff qty is set.
			// Used for google calendar events in most cases.
			$staff[0] = $product_available_qty;

			#print '<pre>'; print_r( date( 'Y-m-d G:i', $slot ) ); print '</pre>';

			// Figure out how much qty have, either based on combined staff quantity,
			// single staff, or just product.
			[$staff, $available_qty, $max_available_qty] = $this->calculate_staff_availability_for_slot(
				$slot,
				$interval,
				$product_staff,
				$product_has_staff,
				$product_duration_unit,
				$product_available_qty
			);

			$slot_day_key = strtotime( 'midnight', $slot );
			if ( $product_is_qty_per_day ) {
				foreach ( $staff as $staff_key => $staff_capacity_for_slot ) {
					$staff_capacity_for_slot = max( 0, (int) $staff_capacity_for_slot );
					if ( ! isset( $day_base_capacity_by_staff[ $staff_key ][ $slot_day_key ] ) ) {
						$day_base_capacity_by_staff[ $staff_key ][ $slot_day_key ] = $staff_capacity_for_slot;
					} else {
						$day_base_capacity_by_staff[ $staff_key ][ $slot_day_key ] = min( $day_base_capacity_by_staff[ $staff_key ][ $slot_day_key ], $staff_capacity_for_slot );
					}
				}
			}

			$qty_scheduled_in_slot   = 0;
			$qty_scheduled_for_staff = [];

			#error_log( var_export( date( 'Y-m-d G:i', $slot ), true ) );
			#error_log( var_export( $staff, true ) );
			#print '<pre>'; print_r( date( 'Y-m-d G:i', $slot ) ); print '</pre>';
			#print '<pre>'; print_r( $max_available_qty ); print '</pre>';
			#print '<pre>'; print_r( $get_available_qty ); print '</pre>';
			#print '<pre>'; print_r( $staff ); print '</pre>';
			#print '<pre>'; print_r( $product_staff ); print '</pre>';
			#print '<pre>'; print_r( $available_qty ); print '</pre>';

			// All staff assigned at once.
			if ( $product_is_staff_assignment_all && $product_staff && is_array( $product_staff ) ) {
				$count_all_staff       = count( $product_staff );
				$count_available_staff = count( $staff ) - 1;

				// Set $available_qty to zero, when any of the staff is not available.
				if ( $count_all_staff !== $count_available_staff ) {
					$available_qty = 0;
				}
			}

			$qty_to_add = 1;

			if ( $existing_appointments ) {
				foreach ( $existing_appointments as $existing_appointment ) {
					// Appointment and Slot start/end timestamps.
					$slot_start        = $slot;
					$slot_end          = strtotime( "+{$interval} minutes", $slot );
					$appointment_start = $existing_appointment->get_start();
					$appointment_end   = $existing_appointment->get_end();

					// Account for padding from BOTH products.
					// We combine this product's padding with the conflicting appointment product's padding.
					// For appointments on the same product, avoid double-counting by using only this product's padding.
					if ( in_array( $product_duration_unit, [ self::DURATION_UNIT_HOUR, self::DURATION_UNIT_MINUTE, self::DURATION_UNIT_DAY ], true ) ) {
						$combined_padding_minutes = (int) ( $padding_duration_in_minutes ?: 0 );
						$appointment_product_id   = $existing_appointment->get_product_id();
						if ( $appointment_product_id && $appointment_product_id !== $product_id ) {
							// Lookup and cache the other product's padding in minutes.
							if ( ! array_key_exists( $appointment_product_id, $appointment_padding_cache ) ) {
								$other_product = $existing_appointment->get_product();
								$appointment_padding_cache[ $appointment_product_id ] = ( $other_product && method_exists( $other_product, 'get_padding_duration_in_minutes' ) ) ? (int) $other_product->get_padding_duration_in_minutes() : 0;
							}
							$combined_padding_minutes += $appointment_padding_cache[ $appointment_product_id ];
						}

						if ( 0 < $combined_padding_minutes ) {
							// Shift appointment window by combined padding to evaluate overlap correctly.
							if ( ! empty( $appointment_start ) ) {
								$appointment_start = strtotime( "-{$combined_padding_minutes} minutes", $appointment_start );
							}
							if ( ! empty( $appointment_end ) ) {
								$appointment_end = strtotime( "+{$combined_padding_minutes} minutes", $appointment_end );
							}
						}
					}
                    // Is within slot?
                    if (! $appointment_start) {
                        continue;
                    }
                    if (! $appointment_end) {
                        continue;
                    }
                    if ($appointment_start >= $slot_end) {
                        continue;
                    }
                    if ($appointment_end <= $slot_start) {
                        continue;
                    }

					#error_log( var_export( $existing_appointment->get_id(), true ) );
					#error_log( var_export( date( 'G:i', $appointment_start ) . ' == ' . date( 'G:i', $appointment_end ), true ) );
					#error_log( var_export( date( 'G:i', $slot_start ) . ' == ' . date( 'G:i', $slot_end ), true ) );

					// Appointment quantity.
					$qty_to_add = $existing_appointment->get_qty() ?: 1;

					// Reset quantity for staff on each loop.
					$qty_scheduled_for_staff = [];

					// Appointment has staff?
					if ( $product_has_staff ) {
						// Get staff IDs. If no staff, make it zero (applies to all).
						$appointment_staff_ids = $existing_appointment->get_staff_ids();
						$appointment_staff_ids = wc_appointments_normalize_staff_ids( $appointment_staff_ids );
						$appointment_staff_ids = [] === $appointment_staff_ids ? [ 0 ] : $appointment_staff_ids;

						if (
							$product_staff
							&& ! is_array( $product_staff )
							&& $appointment_staff_ids
							&& is_array( $appointment_staff_ids )
							&& in_array( $product_staff, $appointment_staff_ids )
						) {
							foreach ( $appointment_staff_ids as $appointment_staff_id ) {
								if ( $existing_appointment->get_product_id() !== $product_id ) {
										$check = apply_filters( 'wc_appointments_check_appointment_product', true, $existing_appointment->get_id(), $product_id );
										$check = apply_filters( 'wc_apointments_check_appointment_product', true, $existing_appointment->get_id(), $product_id ); // BC for old hook name
										if ( $check ) {
											$qty_to_add = $this->get_available_qty( $appointment_staff_id );
										}
									}
								$qty_to_add                      = apply_filters( 'wc_appointments_check_appointment_qty', $qty_to_add, $existing_appointment, $this );
								$qty_scheduled_for_staff[]       = $qty_to_add;
								$appointment_staff_id_qty_scheduled = ( $staff[ $appointment_staff_id ] ?? 0 ) - $qty_to_add;
								$appointment_staff_id_qty_available = max( $appointment_staff_id_qty_scheduled, 0 ); #when negative, turn to zero.
								$staff[ $appointment_staff_id ]     = $appointment_staff_id_qty_available;
							}
							$qty_scheduled_in_slot += max( $qty_scheduled_for_staff );
							#print '<pre>'; print_r( date( 'Y-m-d G:i', $slot ) ); print '</pre>';
							#print '<pre>'; print_r( $qty_scheduled_for_staff ); print '</pre>';
							#print '<pre>'; print_r( $qty_scheduled_in_slot ); print '</pre>';

						} elseif (
							$product_staff
							&& is_array( $product_staff )
							&& $appointment_staff_ids
							&& is_array( $appointment_staff_ids )
							&& array_intersect( $product_staff, $appointment_staff_ids )
						) {
							foreach ( $appointment_staff_ids as $appointment_staff_id ) {
								if ( $existing_appointment->get_product_id() !== $product_id ) {
										$check = apply_filters( 'wc_appointments_check_appointment_product', true, $existing_appointment->get_id(), $product_id );
										$check = apply_filters( 'wc_apointments_check_appointment_product', true, $existing_appointment->get_id(), $product_id ); // BC for old hook name
										if ( $check ) {
											$qty_to_add = $this->get_available_qty( $appointment_staff_id );
										}
									}
								$qty_to_add                      = apply_filters( 'wc_appointments_check_appointment_qty', $qty_to_add, $existing_appointment, $this );
								$qty_scheduled_for_staff[]       = $qty_to_add;
								$appointment_staff_id_qty_scheduled = ( $staff[ $appointment_staff_id ] ?? 0 ) - $qty_to_add;
								$appointment_staff_id_qty_available = max( $appointment_staff_id_qty_scheduled, 0 ); #when negative, turn to zero.
								$staff[ $appointment_staff_id ]     = $appointment_staff_id_qty_available;

								// All staff assigned at once.
								if ( $product_is_staff_assignment_all && 0 === $appointment_staff_id_qty_available ) {
									// Set $available_qty to zero, staff is not available.
									$available_qty = 0;
								}
							}
							$qty_scheduled_in_slot += max( $qty_scheduled_for_staff );

						} else {
							$staff[0] = ( $staff[0] ?? 0 ) - $qty_to_add;
						}
					} else {
						$qty_scheduled_in_slot += $qty_to_add;
						$staff[0]               = ( $staff[0] ?? 0 ) - $qty_to_add;
					}

					$qty_scheduled_in_slot = apply_filters(
					    'wc_appointments_qty_scheduled_in_slot',
					    $qty_scheduled_in_slot,
					    $existing_appointment,
					    $this,
					);
				}
			}

			if ( ! $available_qty && ! $include_sold_out ) {
				continue;
			}

			// Available qty in slot.
			$qty_available_in_slot = $available_qty - $qty_scheduled_in_slot;

			if ( $product_is_qty_per_day ) {
				$slot_day_key      = strtotime( 'midnight', $slot );
				$day_remaining_sum = 0;

				$staff_ids_for_day = wc_appointments_normalize_staff_ids( $product_staff );
				$staff_ids_for_day = array_filter( array_map( 'intval', $staff_ids_for_day ), 'is_numeric' );

				if ( $product_has_staff ) {
					if ( $product_is_staff_assignment_all && $product_staff && is_array( $product_staff ) ) {
						$day_remaining_min = null;
						foreach ( $staff_ids_for_day as $staff_for_day ) {
							$base_for_day   = $day_base_capacity_by_staff[ $staff_for_day ][ $slot_day_key ] ?? ( isset( $staff[ $staff_for_day ] ) ? max( 0, (int) $staff[ $staff_for_day ] + $qty_scheduled_in_slot ) : 0 );
							$booked_for_day = $day_booked_by_staff[ $staff_for_day ][ $slot_day_key ] ?? 0;
							if ( isset( $day_booked_by_staff[0][ $slot_day_key ] ) && 0 !== $staff_for_day ) {
								$booked_for_day += $day_booked_by_staff[0][ $slot_day_key ];
							}
							$day_remaining  = max( 0, $base_for_day - $booked_for_day );

							if ( isset( $staff[ $staff_for_day ] ) ) {
								$staff[ $staff_for_day ] = min( $staff[ $staff_for_day ], $day_remaining );
							}

							$day_remaining_min = null === $day_remaining_min ? $day_remaining : min( $day_remaining_min, $day_remaining );
						}
						if ( null === $day_remaining_min ) {
							$day_remaining_min = 0;
						}
						$day_remaining_sum = (int) $day_remaining_min;
					} elseif ( is_array( $product_staff ) ) {
						foreach ( $staff_ids_for_day as $staff_for_day ) {
							$base_for_day   = $day_base_capacity_by_staff[ $staff_for_day ][ $slot_day_key ] ?? ( isset( $staff[ $staff_for_day ] ) ? max( 0, (int) $staff[ $staff_for_day ] + $qty_scheduled_in_slot ) : 0 );
							$booked_for_day = $day_booked_by_staff[ $staff_for_day ][ $slot_day_key ] ?? 0;
							if ( isset( $day_booked_by_staff[0][ $slot_day_key ] ) && 0 !== $staff_for_day ) {
								$booked_for_day += $day_booked_by_staff[0][ $slot_day_key ];
							}
							$day_remaining  = max( 0, $base_for_day - $booked_for_day );

							if ( isset( $staff[ $staff_for_day ] ) ) {
								$staff[ $staff_for_day ] = min( $staff[ $staff_for_day ], $day_remaining );
							}

							$day_remaining_sum += $day_remaining;
						}
						$staff[0] = $day_remaining_sum;
					} else {
						$staff_for_day  = (int) $product_staff;
						$base_for_day   = $day_base_capacity_by_staff[ $staff_for_day ][ $slot_day_key ] ?? ( isset( $staff[ $staff_for_day ] ) ? max( 0, (int) $staff[ $staff_for_day ] + $qty_scheduled_in_slot ) : $product_available_qty );
						$booked_for_day = $day_booked_by_staff[ $staff_for_day ][ $slot_day_key ] ?? 0;
						if ( isset( $day_booked_by_staff[0][ $slot_day_key ] ) && 0 !== $staff_for_day ) {
							$booked_for_day += $day_booked_by_staff[0][ $slot_day_key ];
						}
						$day_remaining  = max( 0, $base_for_day - $booked_for_day );

						if ( isset( $staff[ $staff_for_day ] ) ) {
							$staff[ $staff_for_day ] = min( $staff[ $staff_for_day ], $day_remaining );
						}
						$day_remaining_sum = $day_remaining;
					}
				} else {
					$base_for_day      = $day_base_capacity_by_staff[0][ $slot_day_key ] ?? $product_available_qty;
					$booked_for_day    = $day_booked_by_staff[0][ $slot_day_key ] ?? 0;
					$day_remaining_sum = max( 0, $base_for_day - $booked_for_day );
					$staff[0]          = min( $staff[0], $day_remaining_sum );
				}

				$qty_available_in_slot = min( $qty_available_in_slot, $day_remaining_sum );

				// Prevent aggregate availability from exceeding the product's per-day cap when no staff is preselected.
				if ( $product_has_staff && ! $staff_id ) {
					$qty_available_in_slot = min( $qty_available_in_slot, $product_available_qty );
					$staff[0]              = min( $staff[0], $product_available_qty );
				}

				if ( 0 >= $qty_available_in_slot && ! $include_sold_out ) {
					continue;
				}
			}

			if ( 0 > $qty_available_in_slot && ! $include_sold_out ) {
				continue;
			}

			$available_slots[ $slot ] = apply_filters(
			    'woocommerce_appointments_time_slot',
			    [
					'scheduled' => $qty_scheduled_in_slot,
					'available' => $qty_available_in_slot,
					'staff'     => $staff,
				],
			    $slot,
			    $qty_scheduled_in_slot,
			    $available_qty,
			    $staff,
			    $this,
			    $staff_id,
			    $existing_appointments,
			);

			#error_log( var_export( date( 'G:i', $slot ), true ) );
			#error_log( var_export( $qty_scheduled_in_slot, true ) );
			#error_log( var_export( $available_slots, true ) );

			#print '<pre>'; print_r( date( 'G:i', $slot ) ); print '</pre>';
			#print '<pre>'; print_r( $qty_scheduled_in_slot ); print '</pre>';
			#print '<pre>'; print_r( $staff ); print '</pre>';
			#print '<pre>'; print_r( $staff_id ); print '</pre>';
			#print '<pre>'; print_r( $available_qty ); print '</pre>';
			#print '<pre>'; print_r( $available_slots ); print '</pre>';
			#print '<pre>'; print_r( $existing_appointments ); print '</pre>';
		}

		return apply_filters(
		    'woocommerce_appointments_time_slots',
		    $available_slots,
		    $slots,
		    $intervals,
		    $time_to_check,
		    $staff_id,
		    $from,
		    $to,
		    $timezone,
		    $this,
		);

	}

	/**
	 * Find available slots and return HTML for the user to choose a slot. Used in class-wc-appointments-ajax.php.
	 *
	 * @param  array $args
	 *     @option  array   $slots          The slots we'll be checking availability for.
	 *     @option  array   $intervals      Array containing 2 items; the interval of the slot (maybe user set), and the base interval for the slot/product.
	 *     @option  integer $staff_id       Resource we're getting slots for. Falls backs to product as a whole if 0.
	 *     @option  integer $time_to_check  Specific time checking.
	 *     @option  integer $from           The starting date for the set of slots.
	 *     @option  integer $to             Ending date for the set of slots.
	 *     @option  string  $timezone       Timezone string.
	 *     @option  integer $timestamp      Selected timestamp.
	 *
	 * @return string
	 *
	 * @version  3.3.0
	 */
	public function get_time_slots_html( $args ) {
		$args = apply_filters(
		    'woocommerce_appointments_time_slots_html_args',
		    wp_parse_args(
		        $args,
		        [
					'available'        => [],
					'intervals'        => [],
					'staff_id'         => 0,
					'time_to_check'    => 0,
					'from'             => 0,
					'to'               => 0,
					'timestamp'        => 0,
					'timezone'         => 'UTC',
					'include_sold_out' => false,
				],
		    ),
		    $this,
		);

		$slots            = $args['available'];
		$intervals        = $args['intervals'];
		$staff_id         = $args['staff_id'];
		$time_to_check    = $args['time_to_check'];
		$from             = $args['from'];
		$to               = $args['to'];
		$timestamp        = $args['timestamp'];
		$timezone         = $args['timezone'];
		$include_sold_out = $args['include_sold_out'];

		$available_slots = $this->get_time_slots(
		    [
				'slots'         => $slots,
				'intervals'     => $intervals,
				'staff_id'      => $staff_id,
				'time_to_check' => $time_to_check,
				'from'          => $from,
				'to'            => $to,
				'timezone'      => $timezone,
			],
		);
		$slots_html      = '';

		#print '<pre>'; print_r( $slots ); print '</pre>';
		#print '<pre>'; print_r( $available_slots ); print '</pre>';

		if ( $available_slots ) {

			// Timezones.
			$timezone_datetime = new DateTime();
			$local_time        = $this->has_timezones() ? wc_appointment_timezone_locale( 'site', 'user', $timezone_datetime->getTimestamp(), wc_appointments_time_format(), $timezone ) : '';
			$site_time         = $this->has_timezones() ? wc_appointment_timezone_locale( 'site', 'user', $timezone_datetime->getTimestamp(), wc_appointments_time_format(), wc_timezone_string() ) : '';

			#print '<pre>'; print_r( $timezone ); print '</pre>';
			#print '<pre>'; print_r( $local_time ); print '</pre>';
			#print '<pre>'; print_r( $site_time ); print '</pre>';

			// Split day into three parts
			$times = apply_filters(
			    'woocommerce_appointments_times_split',
			    [
					'morning'   => [
						'name' => __( 'Morning', 'woocommerce-appointments' ),
						'from' => strtotime( '00:00' ),
						'to'   => strtotime( '12:00' ),
					],
					'afternoon' => [
						'name' => __( 'Afternoon', 'woocommerce-appointments' ),
						'from' => strtotime( '12:00' ),
						'to'   => strtotime( '17:00' ),
					],
					'evening'   => [
						'name' => __( 'Evening', 'woocommerce-appointments' ),
						'from' => strtotime( '17:00' ),
						'to'   => strtotime( '24:00' ),
					],
				],
			);

			$slots_html .= "<div class=\"slot_row\">";
			foreach ( $times as $k => $v ) {
				$slots_html .= "<ul class=\"slot_column $k\">";
				$slots_html .= '<li class="slot_heading">' . $v['name'] . '</li>';
				$count       = 0;

			 	foreach ( $available_slots as $slot => $quantity ) {

					$local_slot   = $this->has_timezones() ? wc_appointment_timezone_locale( 'site', 'user', $slot, 'U', $timezone ) : $slot;
					$display_slot = ( $this->has_timezones() && $local_time !== $site_time ) ? $local_slot : $slot;

					/*
					// Used for testing.
					if ( '23:00' === date_i18n( 'H:i', $local_slot ) ) {
						print '<pre>'; print_r( date_i18n( 'Y.m.d H:i', $slot ) . ' -- ' . date_i18n( 'Y.m.d H:i', $local_slot ) ); print '</pre>';
					}
					*/

					// Skip dates that are from different days (used for timezones).
					if ( date( 'Y.m.d', $timestamp ) !== date_i18n( 'Y.m.d', $local_slot ) ) {
						continue;
					}

					if ( strtotime( date( 'G:i', $display_slot ) ) >= $v['from'] && strtotime( date( 'G:i', $display_slot ) ) < $v['to'] ) {
						$selected = $time_to_check && date( 'G:i', $slot ) === date( 'G:i', $time_to_check ) ? ' selected' : '';

						#print '<pre>'; print_r( date( 'Hi', $slot ) ); print '</pre>';
						#print '<pre>'; print_r( $quantity ); print '</pre>';

						// Available quantity should be max per staff and not max overall.
						if ( is_array( $quantity['staff'] ) && 1 < count( $quantity['staff'] ) ) {
							unset( $quantity['staff'][0] );
							$quantity_available     = absint( max( $quantity['staff'] ) );
							$quantity_all_available = absint( array_sum( $quantity['staff'] ) );
							if ( 0 === $staff_id && $quantity_available !== $quantity_all_available ) {
								$quantity_available = ( $this->get_qty_max() < $quantity_available ) ? $this->get_qty_max() : $quantity_available;
								/* translators: %d: quantity */
								$spaces_left = sprintf( _n( '%d max', '%d max', $quantity_available, 'woocommerce-appointments' ), $quantity_available );
								/* translators: %d: quantity */
								$spaces_left .= ', ' . sprintf( _n( '%d left', '%d left', $quantity_all_available, 'woocommerce-appointments' ), $quantity_all_available );
							} else {
								/* translators: %d: quantity */
								$spaces_left = sprintf( _n( '%d left', '%d left', $quantity_available, 'woocommerce-appointments' ), $quantity_available );
							}
						} else {
							$quantity_available = absint( $quantity['available'] );
							/* translators: %d: quantity */
							$spaces_left = sprintf( _n( '%d left', '%d left', $quantity_available, 'woocommerce-appointments' ), $quantity_available );
						}

						#print '<pre>'; print_r( date( 'Hi', $slot ) ); print '</pre>';
						#print '<pre>'; print_r( $quantity_available ); print '</pre>';
						#print '<pre>'; print_r( $quantity_all_available ); print '</pre>';
						#print '<pre>'; print_r( $staff_id ); print '</pre>';

						if ( 0 < $quantity_available ) {
							if ( $quantity['scheduled'] ) {
								/* translators: %d: quantity */
				 				$slot_html = "<li class=\"slot$selected\" data-slot=\"" . esc_attr( date( 'Hi', $display_slot ) ) . "\" data-remaining=\"" . esc_attr( $quantity['available'] ) . "\"><a href=\"#\" data-value=\"" . date_i18n( 'G:i', $display_slot ) . "\">" . date_i18n( wc_appointments_time_format(), $display_slot ) . " <small class=\"spaces-left\">" . $spaces_left . "</small></a></li>";
				 			} else {
				 				$slot_html = "<li class=\"slot$selected\" data-slot=\"" . esc_attr( date( 'Hi', $display_slot ) ) . "\" data-remaining=\"" . esc_attr( $quantity['available'] ) . "\"><a href=\"#\" data-value=\"" . date_i18n( 'G:i', $display_slot ) . "\">" . date_i18n( wc_appointments_time_format(), $display_slot ) . "</a></li>";
				 			}
							$slots_html .= apply_filters( 'woocommerce_appointments_time_slot_html', $slot_html, $display_slot, $quantity, $time_to_check, $staff_id, $timezone, $this, $spaces_left, [] );
						} elseif ( 0 === $quantity_available && $include_sold_out ) {
							/* translators: %d: quantity */
							$slot_html   = "<li class=\"slot$selected\" data-slot=\"" . esc_attr( date( 'Hi', $display_slot ) ) . "\" data-remaining=\"0\"><span data-value=\"" . date_i18n( 'G:i', $display_slot ) . "\">" . date_i18n( wc_appointments_time_format(), $display_slot ) . " <small class=\"spaces-left\">" . $spaces_left . "</small></span></li>";
							$slots_html .= apply_filters( 'woocommerce_appointments_time_slot_html', $slot_html, $display_slot, $quantity, $time_to_check, $staff_id, $timezone, $this, $spaces_left, [] );
						} else {
							continue;
						}
					} else {
						continue;
					}

					$count++;
			 	}

				if ( 0 === $count ) {
					$slots_html .= '<li class="slot slot_empty">' . __( '&#45;', 'woocommerce-appointments' ) . '</li>';
				}

				$slots_html .= "</ul>";
			}

			$slots_html .= "</div>";
		}

	 	return apply_filters( 'woocommerce_appointments_time_slots_html', $slots_html, $slots, $intervals, $time_to_check, $staff_id, $from, $to, $timezone, $this );
	}

	/**
	 * Get the first available slot with optimized early exit strategy.
	 * This method is designed for performance when only the first available slot is needed.
	 *
	 * @param int $start_date Starting timestamp to search from
	 * @param int $max_date Maximum timestamp to search until
	 * @param int $staff_id Optional staff ID to filter by
	 * @param int $increment_days Number of days to increment in each iteration (default: 1)
	 * @return int|false First available slot timestamp or false if none found
	 */
	public function get_first_available_slot( $start_date, $max_date, $staff_id = 0, $increment_days = 1 ) {
		$current_date = $start_date;
		$increment_seconds = $increment_days * DAY_IN_SECONDS;

		// Use indexed availability if available and within horizon
		$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 && ( $max_date <= $horizon_ts );
		}

		while ( $current_date <= $max_date ) {
			// Calculate end of current increment period
			$period_end = min( $current_date + $increment_seconds, $max_date );

			// Use lazy slot generation to get just the first available slot
			// Important: Do NOT span timezone across adjacent days here.
			// When choosing a default date, we want to search day-by-day in site-local time,
			// otherwise a cross-day span can bias towards the next day even when today has slots.
			// Therefore, set $timezone_span to false so each iteration only considers the current day range.
			$first_slot = $this->get_slots_in_range_lazy( $current_date, $period_end, [], $staff_id, [], false, false, 1 );

			if ( false !== $first_slot && ! empty( $first_slot ) ) {
				// Get the first slot from the array
				$slot = is_array( $first_slot ) ? $first_slot[0] : $first_slot;
                // Use indexed availability if enabled
                if ( $use_indexed && class_exists( 'WC_Appointments_Controller' ) && method_exists( 'WC_Appointments_Controller', 'get_cached_available_slots' ) ) {
						$available_slots = \WC_Appointments_Controller::get_cached_available_slots(
						    [
								'product'   => $this,
								'slots'     => [ $slot ],
								'staff_id'  => $staff_id,
								'from'      => $slot,
								'to'        => $slot + $this->get_duration() * 60,
							],
						);
					} else {
						// Fall back to original method
						$available_slots = $this->get_available_slots(
						    [
								'slots'    => [ $slot ],
								'staff_id' => $staff_id,
								'from'     => $slot,
								'to'       => $slot + $this->get_duration() * 60,
							],
						);
					}
                // If this slot is available, return it immediately (early exit)
                if ( ! empty( $available_slots ) && in_array( $slot, $available_slots ) ) {
						return $slot;
					}
			}

			// Move to next increment period
			$current_date += $increment_seconds;
		}

		// No available slot found
		return false;
	}
}
