/* global wc_appointment_modal_js_params, WC_PAO */
jQuery( function( $ ) {
	'use strict';

	var wp = window.wc_appointments_writepanel;
	if ( !wp ) {
		return;
	}

	// Extend the existing writepanel object with modal-specific logic.
	$.extend( wp, {
		// ============================================================================
		// Section: Bootstrapping & Event Binding
		// ----------------------------------------------------------------------------
		// Scopes queries to the active modal and wires top-level event handlers.
		// Keep these lightweight and focused on delegating to helpers.
		// ============================================================================
		// Caches to reduce AJAX calls
		_productDetailsCache: {},
		_productAddonsCache: {},
		_costCache: {},
		_costInFlightKey: null,
		_availabilityCache: {},
		_availabilityCacheTTL: 5000,
		_focusRetryCount: 0,
		// Track last availability params key to avoid redundant checks
		_lastAvailabilityKey: null,
		// Track calendar-selected range to prevent defaults overriding it on first open
		_prefillRange: null,
		// Prevent PAO sync-triggered updates from recursively scheduling cost recalculation
		_paoSilentUpdate: false,
		// Helper to scope queries to the active modal
		findInModal: function( selector ) {
			var $modal = $( '.wc-backbone-modal' );
			return $modal.length ? $modal.find( selector ) : $( selector );
		},
		schedule: function( fn, delay ) {
			var d = ( delay || 0 );
			if ( 0 === d && 'function' === typeof window.requestAnimationFrame ) {
				try { window.requestAnimationFrame( fn ); return; } catch ( e ) {}
			}
			setTimeout( fn, d );
		},
		scheduleMany: function( fns, delay ) {
			var arr = Array.isArray( fns ) ? fns.slice() : [];
			var d = ( delay || 16 );
			if ( !arr.length ) { return; }
			// Always use setTimeout with delay to prevent blocking, even if requestIdleCallback is available
			// This ensures each function runs in its own tick and doesn't block the main thread
			function step() {
				if ( !arr.length ) { return; }
				var fn = arr.shift();
				try {
					if ( 'function' === typeof fn ) {
						fn();
					}
				} catch ( e ) {}
				if ( arr.length ) {
					setTimeout( step, d );
				}
			}
			setTimeout( step, d );
		},
		initModal: function() {
			// Ensure bindings are applied only once per page lifecycle
			if ( wp._modalInitialized ) {
				return;
			}
			wp._modalInitialized = true;
			$( document ).on( 'click', '.wc-appointment-modal-trigger', wp.openModal );
			$( document ).on( 'click', '.wc-backbone-modal-dialog  .modal-close', wp.closeModal );
			$( document ).on( 'change', '.wc-backbone-modal #appointment_product_id', wp.onProductChange );
			$( document ).on( 'change', '.wc-backbone-modal #appointment_date', wp.onDateChange );
			$( document ).on( 'change', '.wc-backbone-modal #appointment_time_from', wp.onStartTimeChange );
			// Trigger checks when end time changes (manual edits or suggestion click)
			$( document ).on( 'change', '.wc-backbone-modal #appointment_time_to', wp.onEndTimeChange );
			// Range changes are unified through onRangeChange; avoid double validate calls
			// (validateForm is invoked inside onRangeChange)
			// Hook additional events to trigger cost calculation
			$( document ).on( 'change', '.wc-backbone-modal #appointment_staff_id', wp.onStaffChange );
			$( document ).on( 'change', '.wc-backbone-modal #appointment_date_from, .wc-backbone-modal #appointment_date_to', wp.onRangeChange );
			$( document ).on( 'change', '.wc-backbone-modal #appointment_month_from, .wc-backbone-modal #appointment_month_to', wp.onRangeChange );
			$( document ).on( 'change', '.wc-backbone-modal #order_type', wp.onOrderTypeChange );
			$( document ).on( 'change', '.wc-backbone-modal #customer_type', wp.onCustomerTypeChange );
			$( document ).on( 'change', '.wc-backbone-modal #customer_user', wp.onCustomerSelected );
			$( document ).on( 'change', '.wc-backbone-modal #_billing_country', wp.onBillingCountryChange );
			$( document ).on( 'click', '.wc-backbone-modal .wc-appointment-billing-details h4', wp.toggleBillingSection );
			$( document ).on( 'click', '.wc-backbone-modal .wc-appointment-addons h4', wp.toggleAddonsSection );
			// Price edit events in footer
			$( document ).on( 'click', '.wc-backbone-modal #appointment_price_edit', wp.onPriceEditClick );
			$( document ).on( 'blur', '.wc-backbone-modal #appointment_override_price', wp.onPriceEditCommit );
			$( document ).on( 'keyup', '.wc-backbone-modal #appointment_override_price', wp.onPriceEditKey );
			$( document ).on( 'click', '.wc-backbone-modal #btn-ok', wp.submitForm );
			$( document ).on( 'change keyup', '.wc-backbone-modal .wc-appointment-modal-form input, .wc-backbone-modal .wc-appointment-modal-form select', wp.validateForm );
			// When Product Add-Ons totals update (incl. duration), re-check availability only if params changed
			$( document ).on( 'woocommerce-product-addons-update', '.wc-backbone-modal #wc-appointment-addons-section', function() {
				wp.maybeCheckAvailability();
			} );
			// Trigger recalculations when quantity changes
			$( document ).on( 'change keyup', '.wc-backbone-modal #appointment_qty', wp.onQtyChange );
			$( document ).on( 'change keyup', '.wc-backbone-modal #appointment_date, .wc-backbone-modal #appointment_time_from, .wc-backbone-modal #appointment_time_to, .wc-backbone-modal #appointment_date_from, .wc-backbone-modal #appointment_date_to, .wc-backbone-modal #appointment_month_from, .wc-backbone-modal #appointment_month_to', wp.updateInputHighlights );
			$( document ).on( 'click', '.wc-backbone-modal #appointment-next-suggestion', wp.onNextSuggestionClick );
			// Overlay suggestion click: set value, trigger change, and update highlighting
			$( document ).on( 'click', '.wc-backbone-modal #appointment_time_from_suggestions .time-option', function() {
				var v = $( this ).data( 'value' );
				var $modal = $( this ).closest( '.wc-backbone-modal' );
				var $list = $modal.find( '#appointment_time_from_suggestions' );
				$modal.find( '#appointment_time_from' ).val( v ).trigger( 'change' );
				// Highlight the selected option and clear previous selection states
				$list.find( '.time-option' ).removeClass( 'is-selected' ).attr( 'aria-selected', 'false' ).find( '.selected-mark' ).remove();
				$( this ).addClass( 'is-selected' ).attr( 'aria-selected', 'true' );
				if ( 0 === $( this ).find( '.selected-mark' ).length ) {
					$( this ).append( '<span class="selected-mark">•</span>' );
				}
				$list.removeClass( 'is-visible' ).attr( 'aria-hidden', 'true' );
			} );
			$( document ).on( 'click', '.wc-backbone-modal #appointment_time_to_suggestions .time-option', function() {
				var v = $( this ).data( 'value' );
				var $modal = $( this ).closest( '.wc-backbone-modal' );
				var $list = $modal.find( '#appointment_time_to_suggestions' );
				$modal.find( '#appointment_time_to' ).val( v ).trigger( 'change' );
				// Highlight the selected option and clear previous selection states
				$list.find( '.time-option' ).removeClass( 'is-selected' ).attr( 'aria-selected', 'false' ).find( '.selected-mark' ).remove();
				$( this ).addClass( 'is-selected' ).attr( 'aria-selected', 'true' );
				if ( 0 === $( this ).find( '.selected-mark' ).length ) {
					$( this ).append( '<span class="selected-mark">•</span>' );
				}
				$list.removeClass( 'is-visible' ).attr( 'aria-hidden', 'true' );
			} );
			// Show overlays on focus and hide on blur (with small delay)
			$( document ).on( 'focus', '.wc-backbone-modal #appointment_time_from', function() {
				var $modal = $( this ).closest( '.wc-backbone-modal' );
				$modal.find( '#appointment_time_from_suggestions' ).addClass( 'is-visible' ).attr( 'aria-hidden', 'false' );
				$( this ).attr( 'aria-expanded', 'true' );
			} );
			$( document ).on( 'blur', '.wc-backbone-modal #appointment_time_from', function() {
				var input = this;
				var $modal = $( this ).closest( '.wc-backbone-modal' );
				setTimeout( function() {
					$modal.find( '#appointment_time_from_suggestions' ).removeClass( 'is-visible' ).attr( 'aria-hidden', 'true' );
					// Collapse overlay state without managing activedescendant; no keyboard index tracking
					$( input ).attr( 'aria-expanded', 'false' );
				}, 150 );
			} );
			$( document ).on( 'focus', '.wc-backbone-modal #appointment_time_to', function() {
				var $modal = $( this ).closest( '.wc-backbone-modal' );
				$modal.find( '#appointment_time_to_suggestions' ).addClass( 'is-visible' ).attr( 'aria-hidden', 'false' );
				$( this ).attr( 'aria-expanded', 'true' );
			} );
			$( document ).on( 'blur', '.wc-backbone-modal #appointment_time_to', function() {
				var input = this;
				var $modal = $( this ).closest( '.wc-backbone-modal' );
				setTimeout( function() {
					$modal.find( '#appointment_time_to_suggestions' ).removeClass( 'is-visible' ).attr( 'aria-hidden', 'true' );
					// Collapse overlay state without managing activedescendant; no keyboard index tracking
					$( input ).attr( 'aria-expanded', 'false' );
				}, 150 );
			} );
			// Add-ons link in form to load/toggle add-on fields
			$( document ).on( 'click', '#appointment_addons_link', function( e ) {
				e.preventDefault();
				var productId = wp.selectedProductId || $( '#appointment_product_id' ).val();
				if ( !productId ) {
					return;
				}
				// If not loaded, fetch add-ons; rely on collapse state for visibility
				if ( !wp._addons_loaded ) {
					wp.loadProductAddons( productId );
				}
				// Do not toggle inner fields directly here; header click already toggles collapse
			} );
			$( window ).on( 'resize', wp.resizeContent );
		},

		// ============================================================================
		// Section: Modal Lifecycle
		// ----------------------------------------------------------------------------
		// Responsible for creating/removing modal DOM and priming state.
		// Keep side-effects minimal; delegate to loaders and initializers.
		// ============================================================================
		openModal: function( e ) {
			if ( e ) {
				e.preventDefault();
			}
			if ( jQuery( '.wc-backbone-modal-dialog' ).length ) {
				return;
			}
			$( 'body' ).append( $( '#tmpl-wc-modal-add-appointment' ).html() );
			$( 'body' ).addClass( 'modal-open' );
			$( '.wc-appointment-status' ).hide();
			wp.resizeContent();
			// Reset any lingering busy count when opening a fresh modal
			// This prevents a stuck header spinner caused by unmatched show/hide calls
			wp._busyCount = 0;
			wp._addons_loaded = false;
			// Capture pending staff from global context so it can be applied after a product is chosen
			try {
				var staffFromCtx = ( window.wcAppointmentsReactCalendar && parseInt( window.wcAppointmentsReactCalendar.staffId, 10 ) ) || 0;
				wp._pendingStaffId = ( 0 < staffFromCtx ) ? staffFromCtx : 0;
			} catch ( _e ) { wp._pendingStaffId = 0; }
			wp.loadProducts();
			// Set a flag so we can focus the product selector once selects are enhanced
			wp._justOpenedModal = true;
			wp._focusRetryCount = 0;
		},

		closeModal: function( e ) {
			if ( e ) {
				e.preventDefault();
			}
			// Proactively clear any busy state before removing the modal DOM
			// Ensures the header spinner is not left active on next open
			wp._busyCount = 0;
			$( '.wc-backbone-modal-main' ).removeClass( 'is-busy' );
			$( '.wc-appointment-modal-loading' ).removeClass( 'is-busy' );
			$( '.wc-backbone-modal-dialog' ).remove();
			$( 'body' ).removeClass( 'modal-open' );
		},

		// Rebuild modal DOM fresh and re-select the chosen product
		rebuildModalForProduct: function( productId, productText, staffIdInit ) {
			// Remove existing modal dialog DOM
			$( '.wc-backbone-modal-dialog' ).remove();
			// Append fresh template
			$( 'body' ).append( $( '#tmpl-wc-modal-add-appointment' ).html() );
			$( 'body' ).addClass( 'modal-open' );
			$( '.wc-appointment-status' ).hide();
			// Reset sizing and state
			wp.resizeContent();
			// Reset busy count so new modal starts in a clean state
			wp._busyCount = 0;
			wp._addons_loaded = false;
			// Capture pending staff to apply after product details load
			try {
				var staffFromCtx = ( window.wcAppointmentsReactCalendar && parseInt( window.wcAppointmentsReactCalendar.staffId, 10 ) ) || 0;
				var sInit = ( staffIdInit !== undefined && null !== staffIdInit ) ? parseInt( staffIdInit, 10 ) : staffFromCtx;
				wp._pendingStaffId = ( 0 < sInit ) ? sInit : 0;
			} catch ( e ) { wp._pendingStaffId = 0; }
			// Clear caches for the target product so we fetch fresh details/add-ons
			if ( productId ) {
				try {
					delete wp._productDetailsCache[ productId ];
				} catch ( e ) {}
				try {
					delete wp._productAddonsCache[ productId ];
				} catch ( e ) {}
			}
			// Initialize base content and enhanced selects
			wp.loadProducts();
			wp.initEnhancedSelects();
			var $sel = $( '#appointment_product_id' );
			if ( $sel.length && productId ) {
				var optText = ( productText || '' ).trim();
				var option = new Option( optText || ( '#' + productId ), productId, true, true );
				$sel.append( option ).trigger( 'change' );
			}
		},

		// ============================================================================
		// Section: Layout & Loading Indicators
		// ----------------------------------------------------------------------------
		// Manages responsive sizing and spinner state; avoid blocking the content.
		// ============================================================================
		resizeContent: function() {
			// Sizing handled in CSS via max-height: 75vh on .wc-appointment-modal-content
		},

		_busyCount: 0,
		showLoading: function() {
			var $main = $( '.wc-backbone-modal-main' );
			var $loading = $main.find( '.wc-appointment-modal-loading' );
			if ( !$loading.length ) {
				return;
			}
			wp._busyCount++;
			// Do not blur or disable content; show header spinner only
			$loading.addClass( 'is-busy' );
		},
		hideLoading: function() {
			var $main = $( '.wc-backbone-modal-main' );
			var $loading = $main.find( '.wc-appointment-modal-loading' );
			if ( !$loading.length ) {
				return;
			}
			wp._busyCount = Math.max( 0, wp._busyCount - 1 );
			if ( 0 === wp._busyCount ) {
				$loading.removeClass( 'is-busy' );
			}
		},
		// Detect background activity within the modal so we can warn users
		// if they click Create while data is still loading.
		getBusyStatus: function() {
			var labels = [];
			var isBusy = false;
			// General header spinner state
			if ( 0 < wp._busyCount ) {
				isBusy = true;
			}
			// Product details request in-flight
			try {
				var pd = wp._productDetailsXHR;
				if ( pd && 4 !== pd.readyState ) {
					isBusy = true;
					labels.push( ( wc_appointment_modal_js_params.i18n && wc_appointment_modal_js_params.i18n.loading_product_details ) || 'Product details' );
				}
			} catch ( e ) {}
			// Product Add-ons request in-flight
			try {
				var pax = wp._productAddonsXHR;
				if ( pax && 4 !== pax.readyState ) {
					isBusy = true;
					labels.push( ( wc_appointment_modal_js_params.i18n && wc_appointment_modal_js_params.i18n.loading_addons ) || 'Add-ons' );
				}
			} catch ( e ) {}
			// Availability check in-flight
			try {
				var ax = wp.availabilityXHR;
				if ( ax && 4 !== ax.readyState ) {
					isBusy = true;
					labels.push( wc_appointment_modal_js_params.i18n_checking_availability || 'Availability' );
				}
			} catch ( e ) {}
			// Price calculation in-flight
			try {
				var cx = wp._costXHR;
				var priceSpinnerActive = $( '#appointment_price_spinner' ).hasClass( 'is-active' );
				if ( priceSpinnerActive || ( cx && 4 !== cx.readyState ) ) {
					isBusy = true;
					labels.push( ( wc_appointment_modal_js_params.i18n && wc_appointment_modal_js_params.i18n.calculating_price ) || 'Price' );
				}
			} catch ( e ) {}
			// Add-ons visible but not initialized yet
			if ( !wp._addons_loaded && $( '#wc-appointment-addons-section' ).is( ':visible' ) ) {
				isBusy = true;
				labels.push( ( wc_appointment_modal_js_params.i18n && wc_appointment_modal_js_params.i18n.loading_addons ) || 'Add-ons' );
			}
			return { busy: isBusy, labels: labels };
		},
		// Show a lightweight spinner next to the footer price
		showPriceLoading: function() {
			var $price = $( '#appointment_footer_price' );
			if ( !$price.length ) {
				return;
			}
			var $spinner = $( '#appointment_price_spinner' );
			if ( !$spinner.length ) {
				$spinner = $( '<span id="appointment_price_spinner" class="spinner"></span>' );
				$price.after( $spinner );
			}
			$spinner.addClass( 'is-active' );
		},
		hidePriceLoading: function() {
			var $spinner = $( '#appointment_price_spinner' );
			if ( $spinner.length ) {
				$spinner.removeClass( 'is-active' );
			}
		},

		// ============================================================================
		// Section: Product Initialization & Change Flow
		// ----------------------------------------------------------------------------
		// Initializes product select, handles product changes, and updates form fields.
		// ============================================================================
		loadProducts: function() {
			$( '#appointment_product_id' ).html( '<option value="">' + wc_appointment_modal_js_params.i18n.select_product + '</option>' );
			$( '.wc-backbone-modal-main' ).removeClass( 'is-busy' );
			$( '.wc-appointment-modal-loading' ).removeClass( 'is-busy' );
			$( '.wc-appointment-modal-form' ).addClass( 'loaded' );
			// Keep details visible, but date stays hidden until product selection
			$( '.wc-appointment-details' ).show();
			$( '.wc-appointment-date-field' ).hide();
			$( '.wc-appointment-time-range, .wc-appointment-date-range, .wc-appointment-month-range' ).hide();
			wp.scheduleMany( [
				function() { wp.initEnhancedSelects(); },
				function() { wp.validateForm(); },
				function() { wp.updateInputHighlights(); },
				function() {
					if ( wp._justOpenedModal ) {
						wp.focusProductSelect(); wp._justOpenedModal = false;
					}
				}
			], 16 );
		},

		onProductChange: function() {
			var productId = $( this ).val();
			// Prevent re-entrant product change handling
			if ( wp._productChanging ) {
				return;
			}
			// If user-driven product change and not already rebuilding, reload modal content fresh
			if ( productId && !wp._rebuildingModal ) {
				var selectedText = $( '#appointment_product_id option:selected' ).text().trim();
				wp._rebuildingModal = true;
				wp.rebuildModalForProduct( productId, selectedText );
				return;
			}
			wp._productChanging = true;
			wp.selectedProductId = productId || null;
			// Abort previous product details request to avoid stale updates
			if ( wp._productDetailsXHR && wp._productDetailsXHR.abort ) {
				try {
					wp._productDetailsXHR.abort();
				} catch ( e ) {}
				wp._productDetailsXHR = null;
			}
			// Reset in-flight product ID tracker
			wp._productDetailsInFlightId = null;
			// Reset add-ons state for new product
			$( '#wc-appointment-addons-fields' ).empty();
			$( '#wc-appointment-addons-section' ).hide();
			wp._addons_loaded = false;
			if ( !productId ) {
				$( '.wc-appointment-details, .wc-appointment-customer, .wc-appointment-order, .wc-appointment-status, .wc-appointment-billing-details' ).hide();
				$( '.wc-appointment-product-row .wc-appointment-qty-field' ).hide();
				wp._productChanging = false;
				wp._rebuildingModal = false;
				return;
			}
			$( '.wc-appointment-details' ).show();
			$( '.wc-appointment-status' ).hide();
			$( '.wc-appointment-date-field, .wc-appointment-time-range, .wc-appointment-date-range, .wc-appointment-month-range' ).hide();

			// Use cached product details to avoid redundant requests
			var _pdCached = wp._productDetailsCache[ productId ];
			if ( _pdCached ) {
				wp.updateFormFields( _pdCached );
				$( '.wc-appointment-customer, .wc-appointment-order, .wc-appointment-status, .wc-appointment-billing-details' ).show();
				$( '.wc-appointment-product-row .wc-appointment-qty-field' ).show();
				if ( !$( '#appointment_qty' ).val() ) {
					$( '#appointment_qty' ).val( '1' ).trigger( 'change' );
				}
				// Ensure Add-ons section header is visible
				$( '#wc-appointment-addons-section' ).show();
				// Auto-load add-ons (if any) to apply required validation like frontend
				wp.loadProductAddons( productId );
				// Clear product change reentrancy flag after cached update
				wp._productChanging = false;
				wp._rebuildingModal = false;
				// Product affects availability; re-check if params changed
				wp.maybeCheckAvailability();
			} else {
				wp.showLoading();
				// Track in-flight product details request for this product
				wp._productDetailsInFlightId = productId;
				wp._productDetailsXHR = $.ajax( {
					url: wc_appointment_modal_js_params.ajax_url,
					type: 'POST',
					data: {
						action: 'wc_appointments_get_product_details',
						product_id: productId,
						nonce: wc_appointment_modal_js_params.nonce_get_product_details
					}
				} ).done( function( response ) {
					// Ignore stale responses for a previous selection
					var currentProduct = $( '#appointment_product_id' ).val();
					if ( wp._productDetailsInFlightId !== productId || currentProduct !== productId ) {
						return;
					}
					if ( response.success ) {
						// Cache product details to reduce redundant calls
						wp._productDetailsCache[ productId ] = response.data;
						wp.updateFormFields( response.data );
						$( '.wc-appointment-customer, .wc-appointment-order, .wc-appointment-status, .wc-appointment-billing-details' ).show();
						$( '.wc-appointment-product-row .wc-appointment-qty-field' ).show();
						if ( !$( '#appointment_qty' ).val() ) {
							$( '#appointment_qty' ).val( '1' ).trigger( 'change' );
						}
						// Ensure Add-ons section header is visible
						$( '#wc-appointment-addons-section' ).show();
						// Auto-load add-ons (if any) to apply required validation like frontend
						wp.loadProductAddons( productId );
					} else {
						alert( response.data || wc_appointment_modal_js_params.i18n.error_loading_product );
					}
				} ).fail( function( xhr, status, error ) {
					console.log( 'Product details AJAX error:', status, error, xhr.responseText );
					alert( wc_appointment_modal_js_params.i18n.error_loading_product );
				} ).always( function() {
					wp.hideLoading();
					wp._productChanging = false;
					wp._rebuildingModal = false;
					// Product affects availability; re-check if params changed
					wp.maybeCheckAvailability();
				} );
			}
		},

		updateFormFields: function( productData ) {
			// Ensure selected product option displays a proper formatted name
			try {
				if ( productData && productData.formatted_name ) {
					var $sel = $( '#appointment_product_id' );
					if ( $sel.length ) {
						var curVal = $sel.val();
						if ( curVal ) {
							var $opt = $sel.find( 'option:selected' );
							if ( $opt.length ) {
								$opt.text( productData.formatted_name );
								try {
									var $container = $sel.next( '.select2' ).find( '.select2-selection__rendered' );
									if ( $container.length ) {
										$container.text( productData.formatted_name );
									}
								} catch ( _ ) {}
							}
						}
					}
				}
			} catch ( e ) {}
			// Populate staff select based on the chosen product and toggle visibility
			if ( productData.has_staff ) {
				var $staffSelect = $( '#appointment_staff_id' );
				var method = $.fn.selectWoo ? 'selectWoo' : ( $.fn.select2 ? 'select2' : null );
				
				// Destroy Select2/SelectWoo if already initialized to ensure clean state
				// Check if Select2/SelectWoo container exists (more reliable than data check)
				if ( method ) {
					try {
						// Check if Select2/SelectWoo is initialized by looking for the container
						var $container = $staffSelect.next( '.select2, .selectWoo' );
						if ( $container.length > 0 || $staffSelect.hasClass( 'select2-hidden-accessible' ) || $staffSelect.hasClass( 'selectWoo-hidden-accessible' ) ) {
							$staffSelect[ method ]( 'destroy' );
						}
					} catch ( e ) {
						// If destroy fails, try to remove any existing container manually
						$staffSelect.next( '.select2-container, .selectWoo-container' ).remove();
					}
				}
				
				$staffSelect.empty();
				var staffIds = [];
				var staffNames = [];
				
				// Safely collect staff data - handle empty/undefined staff object
				if ( productData.staff && typeof productData.staff === 'object' ) {
					$.each( productData.staff, function( id, name ) {
						staffIds.push( String( id ) );
						staffNames.push( String( name ) );
					} );
				}

				// Handle "All Staff Assigned" case
				$( '.wc-appointment-staff-field .description' ).remove();

				if ( 'all' === productData.staff_assignment ) {
					// All staff together - disable field and show info
					var infoText = ( wc_appointment_modal_js_params.i18n.all_staff_assigned || 'All staff assigned' ) + ': ' + staffNames.join( ', ' );
					$staffSelect.append( '<option value="" selected="selected">' + infoText + '</option>' );

					if ( method ) {
						$staffSelect[ method ]( { minimumResultsForSearch: 10 } );
					}

					$staffSelect.prop( 'disabled', true );
					if ( method ) {
						try {
							$staffSelect[ method ]( 'enable', false );
						} catch ( e ) {}
					}
				} else {
					// Not "all together" - enable field for selection
					// Add placeholder option
					$staffSelect.append( '<option value="">' + wc_appointment_modal_js_params.i18n.any_staff + '</option>' );
					
					// Add staff options if available
					if ( productData.staff && typeof productData.staff === 'object' ) {
						$.each( productData.staff, function( id, name ) {
							var opt = '<option value="' + String( id ) + '">' + String( name ) + '</option>';
							$staffSelect.append( opt );
						} );
					}

					// Ensure field is enabled BEFORE initializing Select2/SelectWoo
					$staffSelect.prop( 'disabled', false );
					
					// Initialize Select2/SelectWoo
					if ( method ) {
						try {
							$staffSelect[ method ]( { minimumResultsForSearch: 10 } );
							// Explicitly enable via Select2/SelectWoo API after initialization
							// Use setTimeout to ensure initialization is complete
							setTimeout( function() {
								try {
									$staffSelect[ method ]( 'enable', true );
								} catch ( e ) {
									// Fallback: ensure prop is set
									$staffSelect.prop( 'disabled', false );
								}
								// Double-check prop is still enabled
								$staffSelect.prop( 'disabled', false );
							}, 0 );
						} catch ( e ) {
							// If initialization fails, ensure field is still enabled
							$staffSelect.prop( 'disabled', false );
						}
					}
					
					// Final safety check - ensure field is enabled
					$staffSelect.prop( 'disabled', false );
				}
				$( '.wc-appointment-staff-field' ).show();

				// Apply pending staff captured at modal build time
				try {
					var sPending = parseInt( wp._pendingStaffId || 0, 10 );
					if ( 0 < sPending ) {
						$staffSelect.val( String( sPending ) ).trigger( 'change' );
						wp._pendingStaffId = 0;
					}
				} catch ( e ) {}
			} else {
				$( '.wc-appointment-staff-field' ).hide();
			}

			var unit = productData.duration_unit || ( productData.has_time ? 'hour' : 'day' );

			// Persist product data for later auto-fill calculations.
			wp.productData = productData;

			// Show base product price initially; keep edit available
			if ( productData.price_html ) {
				$( '#appointment_footer_price' ).html( productData.price_html );
			} else if ( productData.price_raw !== undefined ) {
				var baseNum = parseFloat( productData.price_raw );
				var baseHtml = wp.formatPriceHTML( baseNum );
				if ( baseHtml ) {
					$( '#appointment_footer_price' ).html( baseHtml );
				}
			}
			if ( productData.price_raw !== undefined ) {
				$( '#appointment_calculated_price_raw' ).val( productData.price_raw );
				// Keep override price in sync with calculated when not editing
				$( '#appointment_override_price_hidden' ).val( productData.price_raw );
			}
			$( '#appointment_price_edit' ).show();

			// Initialize time dropdowns and step based on interval
			if ( 'function' === typeof wp.ensureTimeDatalists ) {
				wp.ensureTimeDatalists();
			}
			if ( 'function' === typeof wp.populateFromTimes ) {
				wp.populateFromTimes( productData );
			}

			$( '.wc-appointment-date-field, .wc-appointment-time-range, .wc-appointment-date-range, .wc-appointment-month-range' ).hide();

			if ( 'minute' === unit || 'hour' === unit ) {
				$( '.wc-appointment-date-field' ).show();
				$( '.wc-appointment-time-range' ).show().find( '.form-field' ).show();
			} else if ( 'day' === unit ) {
				$( '.wc-appointment-date-range' ).show().find( '.form-field' ).show();
			} else if ( 'month' === unit ) {
				$( '.wc-appointment-month-range' ).show().find( '.form-field' ).show();
			} else {
				$( '.wc-appointment-date-field' ).show();
				$( '.wc-appointment-time-range' ).show().find( '.form-field' ).show();
			}

			$( '#appointment_date' ).prop( 'required', 'minute' === unit || 'hour' === unit );
			$( '#appointment_time_from' ).prop( 'required', 'minute' === unit || 'hour' === unit );
			$( '#appointment_time_to' ).prop( 'required', 'minute' === unit || 'hour' === unit );
			$( '#appointment_date_from' ).prop( 'required', 'day' === unit );
			$( '#appointment_date_to' ).prop( 'required', 'day' === unit );
			$( '#appointment_month_from' ).prop( 'required', 'month' === unit );
			$( '#appointment_month_to' ).prop( 'required', 'month' === unit );

			$( '.wc-appointment-duration-field' ).hide();

			wp.initDatePicker( productData );
			if ( ( 'minute' === unit || 'hour' === unit ) && $( '#appointment_date' ).val() ) {
				wp.onDateChange.call( $( '#appointment_date' )[ 0 ] );
				var pr = wp._prefillRange || {};
				var hasEndPrefill = !!( pr.endTs && 0 < parseInt( pr.endTs, 10 ) );
				var fromVal = wp.findInModal( '#appointment_time_from' ).val();
				if ( fromVal && !hasEndPrefill ) {
					wp.applyCombinedDurationToEnd();
				}
			}
			wp.validateForm();
			// Trigger cost calculation after fields update
			wp.scheduleCalculateCost();
		},

		// ============================================================================
		// Section: Product Add-Ons
		// ----------------------------------------------------------------------------
		// Loads, renders, and syncs Product Add-Ons; integrates PAO totals.
		// ============================================================================
		loadProductAddons: function( productId ) {
			if ( !productId ) {
				$( '#wc-appointment-addons-section' ).hide();
				// Remove required indicator if section is hidden
				wp.updateAddonsHeaderRequiredIndicator();
				return;
			}

			// Use cached HTML if we already fetched add-ons for this product
			wp._productAddonsCache = wp._productAddonsCache || {};
			var cachedHtml = wp._productAddonsCache[ productId ];
			if ( cachedHtml ) {
				wp.applyAddonsHtml( cachedHtml );
				return;
			}

			wp.showLoading();
			// Abort previous add-ons request to avoid stale render
			if ( wp._productAddonsXHR && wp._productAddonsXHR.abort ) {
				try {
					wp._productAddonsXHR.abort();
				} catch ( e ) {}
				wp._productAddonsXHR = null;
			}
			wp._productAddonsXHR = $.ajax( {
				url: wc_appointment_modal_js_params.ajax_url,
				type: 'POST',
				data: {
					action: 'wc_appointments_get_product_addons',
					product_id: productId,
					nonce: wc_appointment_modal_js_params.nonce_get_product_details
				}
			} ).done( function( response ) {
				if ( response.success && response.data && response.data.html ) {
					// Cache add-ons HTML to avoid repeated requests for same product
					wp._productAddonsCache[ productId ] = response.data.html;
					// Render and initialize add-ons section
					wp.applyAddonsHtml( response.data.html );
				} else {
					$( '#wc-appointment-addons-section' ).hide();
					wp.updateAddonsHeaderRequiredIndicator();
				}
			} ).fail( function( xhr, status, error ) {
				console.log( 'Product add-ons AJAX error:', status, error, xhr.responseText );
				$( '#wc-appointment-addons-fields' ).hide();
				wp.updateAddonsHeaderRequiredIndicator();
			} ).always( function() {
				wp.hideLoading();
			} );
		},

		// ============================================================================
		// Section: Date & Time Inputs
		// ----------------------------------------------------------------------------
		// Handles date/time field interactions, suggests ranges, and highlights inputs.
		// ============================================================================
		onDateChange: function() {
			var date = $( this ).val();
			if ( date ) {
				$( '#appointment_time_from' ).prop( 'disabled', false );
				if ( 'function' === typeof wp.populateFromTimes ) {
					wp.populateFromTimes( wp.productData || {} );
				}
				wp.validateForm();
				// Recalculate cost when date changes
				wp.scheduleCalculateCost();
				wp.updateInputHighlights();
				// Date affects availability; re-check only if params changed
				wp.maybeCheckAvailability();
			}
		},

		onStartTimeChange: function() {
			var $from = $( '#appointment_time_from' );
			var $to = $( '#appointment_time_to' );
			var fromVal = $from.val();

			if ( !fromVal ) {
				$to.val( '' ).prop( 'disabled', true ).removeAttr( 'min' );
				// Hide and clear 'to' overlay suggestions
				var $toOverlay = $( '#appointment_time_to_suggestions' );
				if ( $toOverlay.length ) {
					$toOverlay.empty().attr( 'aria-hidden', 'true' ).removeClass( 'is-visible' );
				}
				wp.validateForm();
				// Clear cost when time is cleared
				wp.scheduleCalculateCost();
				return;
			}

			$to.prop( 'disabled', false ).removeAttr( 'min' );

			// Auto-fill "to" time by adding combined product + add-ons duration to "from" (wrap past midnight).
			var pd = wp.productData || {};
			var unit = pd.duration_unit || ( pd.has_time ? 'hour' : 'day' );
			var duration = parseInt( pd.duration || 0, 10 );
			if ( 'minute' === unit || 'hour' === unit ) {
				var combinedMinutes = wp.getCombinedDurationMinutes( pd );
				var baseMinutes = duration ? ( 'hour' === unit ? ( duration * 60 ) : duration ) : 60;
				var addMinutes = !isNaN( combinedMinutes ) && 0 !== combinedMinutes ? combinedMinutes : baseMinutes;
				var parts = fromVal.split( ':' );
				var startMin = ( parseInt( parts[ 0 ], 10 ) * 60 ) + parseInt( parts[ 1 ], 10 );
				var endMin = startMin + addMinutes;
				const FULL_DAY_MIN = 24 * 60;
				var norm = ( ( endMin % FULL_DAY_MIN ) + FULL_DAY_MIN ) % FULL_DAY_MIN;
				var endH = ( '0' + Math.floor( norm / 60 ) ).slice( -2 );
				var endM = ( '0' + ( norm % 60 ) ).slice( -2 );
				var toStr = endH + ':' + endM;
				$to.val( toStr );
			}

			// Populate "to" dropdown suggestions across up to 24h span from start
			if ( 'function' === typeof wp.populateToTimes ) {
				wp.populateToTimes( fromVal );
			}

			wp.validateForm();
			// Recalculate cost when time changes
			wp.scheduleCalculateCost();
			wp.updateInputHighlights();
			// Time affects availability; re-check only if params changed
			wp.maybeCheckAvailability();
		},

		onEndTimeChange: function() {
			// Minimal handler for 'to' time changes to refresh availability
			wp.validateForm();
			wp.scheduleCalculateCost();
			wp.updateInputHighlights();
			wp.maybeCheckAvailability();
		},

		// Ensure datalist elements and overlay containers exist for time inputs
		ensureTimeDatalists: function() {
			// Use overlay-only suggestions; no HTML5 datalists are used.
			// Overlay containers are created/managed below.
			// Enforce full-day range for "from" time input
			wp.findInModal( '#appointment_time_from' ).attr( 'min', '00:00' ).attr( 'max', '23:59' );

			// Ensure suggestion overlays exist scoped to the active modal
			if ( !wp.findInModal( '#appointment_time_from_suggestions' ).length ) {
				wp.findInModal( '#appointment_time_from' ).after( '<div id="appointment_time_from_suggestions" class="time-suggestions" role="listbox" aria-label="Time suggestions" aria-hidden="true"></div>' );
			}
			if ( !wp.findInModal( '#appointment_time_to_suggestions' ).length ) {
				wp.findInModal( '#appointment_time_to' ).after( '<div id="appointment_time_to_suggestions" class="time-suggestions" role="listbox" aria-label="Time suggestions" aria-hidden="true"></div>' );
			}
			// Styles now live in appointment-admin-modal.css/appointment-admin-modal.scss; no inline CSS injection needed.
		},

		// Get base interval in minutes from product data
		getIntervalMinutes: function( pd ) {
			pd = pd || ( wp.productData || {} );
			var u = ( pd.interval_unit || '' ).toLowerCase();
			var v = parseInt( pd.interval || 0, 10 );
			if ( !v || !u ) {
				return 60;
			}
			if ( 'hour' === u ) {
				return v * 60;
			}
			if ( 'minute' === u ) {
				return v;
			}
			return 60;
		},

		// Helpers to compute combined appointment duration (base + add-ons)
		getBaseDurationMinutes: function( pd ) {
			pd = pd || ( wp.productData || {} );
			var unit = pd.duration_unit || ( pd.has_time ? 'hour' : 'day' );
			var duration = parseInt( pd.duration || 0, 10 );
			if ( 'hour' === unit ) {
				return duration * 60;
			}
			if ( 'minute' === unit ) {
				return duration;
			}
			return 0;
		},
		getAddonDurationMinutes: function() {
			var v = parseInt( $( '#wc_appointments_field_addons_duration' ).val() || '0', 10 );
			return isNaN( v ) ? 0 : v;
		},
		getCombinedDurationMinutes: function( pd ) {
			return ( wp.getBaseDurationMinutes( pd ) + wp.getAddonDurationMinutes() );
		},
		// Visually mark inputs that have values
		updateInputHighlights: function() {
			var ids = [ '#appointment_date', '#appointment_time_from', '#appointment_time_to', '#appointment_date_from', '#appointment_date_to', '#appointment_month_from', '#appointment_month_to' ];
			ids.forEach( function( sel ) {
				var $input = wp.findInModal( sel );
				var $wrap = $input.closest( '.form-field' );
				if ( !$wrap.length ) {
					return;
				}
				if ( $input.val() ) {
					$wrap.addClass( 'has-value' );
				} else {
					$wrap.removeClass( 'has-value' );
				}
			} );
		},
		buildAvailabilityKey: function( productId, data ) {
			return [
				'p=', productId,
				'&staff=', data.appointment_staff_id || '',
				'&date=', data.appointment_date || '',
				'&df=', data.appointment_date_from || '',
				'&dt=', data.appointment_date_to || '',
				'&mf=', data.appointment_month_from || '',
				'&mt=', data.appointment_month_to || '',
				'&tf=', data.appointment_time_from || '',
				'&tt=', data.appointment_time_to || '',
				'&qty=', ( data.quantity || 1 ),
				'&dur=', data.duration || 0
			].join( '' );
		},
		extractInnerStrong: function( html ) {
			var h = ( 'string' === typeof html ) ? html : ( html && html.html ? html.html : '' );
			var $tmp = $( '<div>' ).html( h || '' );
			var $s = $tmp.find( 'strong' ).first();
			return $s.length ? $s.html() : ( h || '' );
		},
		// Lightweight client-side price formatter using product price_html as a template
		formatPriceHTML: function( amount ) {
			var num = parseFloat( amount );
			if ( isNaN( num ) ) {
				return '';
			}
			var decimals = 2;
			var formatted = num.toFixed( decimals );
			var base = ( wp.productData && wp.productData.price_html ) ? wp.productData.price_html : '';
			var $tmp = $( '<div>' ).html( base );
			var $amount = $tmp.find( '.woocommerce-Price-amount' ).first();
			var $sym = $tmp.find( '.woocommerce-Price-currencySymbol' ).first();
			var symbolHTML = $sym.length ? $( '<div>' ).append( $sym.clone() ).html() : '';
			var symbolBefore = true;
			if ( $sym.length ) {
				var text = ( $amount.length ? $amount.text() : $tmp.text() ) || '';
				var idxSym = text.indexOf( $sym.text() );
				var idxNum = text.search( /[0-9]/ );
				symbolBefore = ( -1 !== idxSym && -1 !== idxNum ) ? ( idxSym < idxNum ) : true;
			}
			var out = '<span class="woocommerce-Price-amount amount"><bdi>';
			if ( symbolBefore && symbolHTML ) {
				out += symbolHTML;
			}
			out += formatted;
			if ( !symbolBefore && symbolHTML ) {
				out += symbolHTML;
			}
			out += '</bdi></span>';
			return out;
		},
		// Keep PAO base price synced with appointment base price
		syncPAOBasePrice: function() {
			try {
				var $addonsSection = $( '#wc-appointment-addons-section' );
				var $totals = $addonsSection.find( '#product-addons-total' );
				if ( !$totals.length ) { return; }
				var productId = parseInt( $( '#appointment_product_id' ).val() || '0', 10 );
				var raw = $( '#appointment_override_price_hidden' ).val();
				if ( '' === raw || undefined === raw ) { raw = $( '#appointment_calculated_price_raw' ).val(); }
				if ( '' === raw || undefined === raw ) { raw = ( wp.productData && wp.productData.price_raw !== undefined ) ? wp.productData.price_raw : ''; }
				var html = $( '#appointment_footer_price' ).html();
				var parsedPrice = null;
				if ( '' !== raw ) {
					var rawNum = parseFloat( ( raw || '' ).toString().replace( ',', '.' ) );
					if ( !isNaN( rawNum ) ) { parsedPrice = rawNum; }
				}
				if ( null === parsedPrice && html ) {
					var text = $( '<div>' ).html( html ).text();
					var m = text.match( /[0-9]+(?:[.,][0-9]{1,2})?/ );
					if ( m ) { parsedPrice = parseFloat( m[ 0 ].replace( ',', '.' ) ); }
				}
				var existingRaw = $totals.data( 'raw-price' );
				var existingPriceVal = $totals.data( 'price' );
				var existingPrice = ( existingPriceVal !== undefined ) ? parseFloat( ( '' + existingPriceVal ).replace( ',', '.' ) ) : NaN;
				var existingProductId = parseInt( ( $totals.data( 'product-id' ) || '0' ), 10 );
				var shouldUpdate = false;
				if ( '' !== raw && existingRaw !== raw ) {
					$totals.data( 'raw-price', raw ).attr( 'data-raw-price', raw );
					shouldUpdate = true;
				}
				if ( null !== parsedPrice && ( isNaN( existingPrice ) || 0.000001 < Math.abs( parsedPrice - existingPrice ) ) ) {
					$totals.data( 'price', parsedPrice ).attr( 'data-price', parsedPrice );
					shouldUpdate = true;
				}
				if ( productId && existingProductId !== productId ) {
					$totals.data( 'product-id', productId ).attr( 'data-product-id', productId );
					shouldUpdate = true;
				}
				// Ensure totals show add-ons subtotal only and hide product line
				$totals.data( 'show-incomplete-sub-total', 1 ).attr( 'data-show-incomplete-sub-total', 1 );
				$totals.data( 'show-sub-total', 1 ).attr( 'data-show-sub-total', 1 );
				$totals.data( 'hide-product-price', 1 ).attr( 'data-hide-product-price', 1 );
				try {
					var lbl = ( 'undefined' !== typeof wc_appointment_modal_js_params && wc_appointment_modal_js_params.i18n_addons_total ) ? wc_appointment_modal_js_params.i18n_addons_total : 'Add-ons total';
					$totals.data( 'i18n_sub_total', lbl ).attr( 'data-i18n_sub_total', lbl );
				} catch ( e2 ) {}
				// Trigger a totals update silently only when the base inputs changed
				if ( shouldUpdate && wp._paoForm && 'function' === typeof wp._paoForm.updateTotals ) {
					wp._paoSilentUpdate = true;
					wp._paoForm.updateTotals();
				}
			} catch ( e ) { /* noop */ }
		},
		// Remove duplicate #product-addons-total inserted after validation_message
		cleanupPAOTotalsDuplicates: function() {
			try {
				var $sec = $( '#wc-appointment-addons-section' );
				if ( !$sec.length ) { return; }
				// Remove any #product-addons-total that appear after .validation_message
				$sec.find( '.validation_message' ).each( function() {
					$( this ).nextAll( '#product-addons-total' ).remove();
				} );
				// If multiple totals remain, keep the first and remove the rest
				var $totalsAll = $sec.find( '#product-addons-total' );
				if ( 1 < $totalsAll.length ) {
					var $keep = $totalsAll.first();
					$totalsAll.not( $keep ).remove();
				}
			} catch ( err ) {}
		},
		applyAddonsHtml: function( html ) {
			$( '#wc-appointment-addons-fields' ).html( html );
			$( '#wc-appointment-addons-section' ).show();
			// Update header required indicator based on rendered add-ons
			wp.updateAddonsHeaderRequiredIndicator();
			// Auto-expand Add-ons when required fields exist so errors/inputs are visible
			( function() {
				var $sec = $( '#wc-appointment-addons-section' );
				var $body = $sec.find( '#wc-appointment-addons-fields, #wc-appointment-addons-container' );
				var requiredSelector = '[aria-required="true"], .wc-pao-addon-wrap.required, .wc-pao-required, input[required], select[required], textarea[required]';
				if ( $body.find( requiredSelector ).length ) {
					$sec.removeClass( 'collapsed' );
				}
			} )();
			wp._addons_loaded = true;
			// Ensure hidden qty field for Product Add-Ons qty-dependent logic
			var $addonsSection = $( '#wc-appointment-addons-section' );
			if ( 0 === $addonsSection.find( 'input.qty' ).length ) {
				$addonsSection.append( '<input type="hidden" class="qty" value="1" />' );
			}
			// Sync hidden qty with the quantity input
			var initialQty = parseInt( $( '#appointment_qty' ).val() || '1', 10 );
			$addonsSection.find( 'input.qty' ).val( initialQty );
			// Ensure hidden fields for add-on duration and cost in the modal form
			var $modalForm = $( '#wc-appointment-modal-form' );
			if ( 0 === $modalForm.find( '#wc_appointments_field_addons_duration' ).length ) {
				$modalForm.append( '<input type="hidden" id="wc_appointments_field_addons_duration" name="wc_appointments_field_addons_duration" value="0" />' );
			}
			if ( 0 === $modalForm.find( '#wc_appointments_field_addons_cost' ).length ) {
				$modalForm.append( '<input type="hidden" id="wc_appointments_field_addons_cost" name="wc_appointments_field_addons_cost" value="0" />' );
			}
			// Initialize Product Add-Ons form to compute totals and trigger events
			if ( window.WC_PAO && 'function' === typeof WC_PAO.Form ) {
				try {
					var $totals = $addonsSection.find( '#product-addons-total' );
					$totals
						.data( 'show-incomplete-sub-total', 1 ).attr( 'data-show-incomplete-sub-total', 1 )
						.data( 'hide-product-price', 1 ).attr( 'data-hide-product-price', 1 );
					// Relabel subtotal for admin modal
					( function() {
						try {
							var lbl = ( 'undefined' !== typeof wc_appointment_modal_js_params && wc_appointment_modal_js_params.i18n_addons_total ) ? wc_appointment_modal_js_params.i18n_addons_total : 'Add-ons total';
							$totals.data( 'i18n_sub_total', lbl ).attr( 'data-i18n_sub_total', lbl );
						} catch ( e ) {}
					} )();
					wp._paoForm = new WC_PAO.Form( $addonsSection );
					// Force totals visibility even when all add-ons are optional
					wp._paoForm.contains_required = true;
					wp._paoForm.show_incomplete_subtotals = true;
					// Seed base price and render totals silently
					wp.syncPAOBasePrice();
					// Clean up duplicate totals containers
					wp.cleanupPAOTotalsDuplicates();
					// Ensure PAO datepickers render above the Backbone modal and inside it
					// This avoids z-index issues and clipping by modal content containers
					( function initModalDatepickers() {
						var $modal = $( '.wc-backbone-modal' );
						var $datepickers = $addonsSection.find( 'input.datepicker' );
						if ( !$datepickers.length ) {
							return;
						}
						$datepickers.each( function() {
							var $dp = $( this );
							// Preserve any existing beforeShow handler from WC_PAO.Form
							var prevBeforeShow = $dp.datepicker( 'option', 'beforeShow' );
							// Append the widget to the modal for proper stacking context
							if ( $modal.length ) {
								try {
									$dp.datepicker( 'option', 'appendTo', $modal );
								} catch ( eAppend ) { /* noop */ }
							}
							// Wrap beforeShow to keep original behavior and enforce our class/z-index
							try {
								$dp.datepicker( 'option', 'beforeShow', function( input, inst ) {
									// Call original handler if present (adds PAO datepicker class)
									if ( 'function' === typeof prevBeforeShow ) {
										prevBeforeShow( input, inst );
									}
									// Move widget into modal to avoid being under the overlay
									if ( $modal.length ) {
										try {
											$dp.datepicker( 'widget' ).appendTo( $modal );
										} catch ( eWidget ) { /* noop */ }
									}
									// Guarantee high z-index even if PAO CSS is not enqueued in admin
									$( '#ui-datepicker-div' ).addClass( 'wc_pao_datepicker wc-pao-datepicker-in-admin-modal' );
								} );
							} catch ( eBefore ) { /* noop */ }
						} );
					} )();
				} catch ( e ) {
					console.warn( 'WC_PAO.Form init failed:', e );
				}
			}
			// Listen for updated_addons to sync hidden fields and recalc appointment cost
			$addonsSection.off( 'updated_addons.appointments' ).on( 'updated_addons.appointments', function() {
				// Keep header required indicator in sync with dynamic add-on visibility/requirements
				wp.updateAddonsHeaderRequiredIndicator();
				// Skip scheduling if this update was triggered by a silent PAO sync
				if ( wp._paoSilentUpdate ) {
					wp._paoSilentUpdate = false;
					return;
				}
				var $totals = $addonsSection.find( '#product-addons-total' );
				var priceData = $totals.data( 'price_data' ) || [];
				var qty = 1;
				var addonDuration = 0;
				var addonCost = 0;
				priceData.forEach( function( item ) {
					var durPu = parseInt( item.duration_raw_pu || 0, 10 );
					var durRaw = parseInt( item.duration_raw || 0, 10 );
					var durBase = durPu || durRaw || 0;
					if ( durBase ) {
						switch ( item.duration_type ) {
							case 'quantity_based':
								addonDuration += durBase * qty;
								break;
							default:
								addonDuration += durBase;
						}
					}
					var costPu = parseFloat( ( item.cost_raw_pu || 0 ).toString().replace( ',', '.' ) );
					var costRaw = parseFloat( ( item.cost_raw || 0 ).toString().replace( ',', '.' ) );
					var hasRaw = !isNaN( costRaw ) && 0 != costRaw;
					var hasPu = !isNaN( costPu ) && 0 != costPu;
					var costBase = hasRaw ? costRaw : ( hasPu ? costPu : 0 );
					if ( 0 != costBase ) {
						addonCost += costBase;
					}
				} );
				$( '#wc_appointments_field_addons_duration' ).val( addonDuration || 0 );
				$( '#wc_appointments_field_addons_cost' ).val( addonCost || 0 );
				var addonVal = addonDuration || 0;

				// Only adjust end date/time when add-on has a duration
				// If add-on has no duration, preserve the user's selected range
				if ( 0 < addonVal ) {
					var fromVal = $( '#appointment_time_from' ).val();
					var showTimeRange = $( '.wc-appointment-time-range' ).is( ':visible' );
					if ( showTimeRange && fromVal ) {
						wp.applyCombinedDurationToEnd();
					}

					// Recompute date/month end when add-ons change and ranges are visible
					var pdR = wp.productData || {};
					var unitR = pdR.duration_unit || ( pdR.has_time ? 'hour' : 'day' );

					if ( 'day' === unitR && $( '.wc-appointment-date-range' ).is( ':visible' ) ) {
						var baseDaysR = parseInt( pdR.duration || 1, 10 );
						var daysR = baseDaysR + ( isNaN( addonVal ) ? 0 : addonVal );
						var fromDR = $( '#appointment_date_from' ).val();
						if ( fromDR ) {
							// TIMEZONE NOTE: Date string (YYYY-MM-DD) represents a date in site timezone.
							// When converted to Date object, it's treated as local date (browser timezone),
							// which is correct for date arithmetic. The resulting date string will be
							// formatted back to YYYY-MM-DD format for the input field.
							var dfr = new Date( fromDR.replace( /-/g, '/' ) );
							dfr.setDate( dfr.getDate() + Math.max( daysR - 1, 0 ) );
							var yR = dfr.getFullYear();
							var mR = ( '0' + ( dfr.getMonth() + 1 ) ).slice( -2 );
							var dR = ( '0' + dfr.getDate() ).slice( -2 );
							$( '#appointment_date_to' ).val( yR + '-' + mR + '-' + dR );
						}
					} else if ( 'month' === unitR && $( '.wc-appointment-month-range' ).is( ':visible' ) ) {
						var baseMonthsR = parseInt( pdR.duration || 1, 10 );
						var monthsR = baseMonthsR + ( isNaN( addonVal ) ? 0 : addonVal );
						var fromYMR = $( '#appointment_month_from' ).val();
						if ( fromYMR ) {
							var partsR = fromYMR.split( '-' );
							var fyR = parseInt( partsR[ 0 ], 10 );
							var fmR = parseInt( partsR[ 1 ], 10 ) - 1;
							var endR = new Date( fyR, fmR + Math.max( monthsR - 1, 0 ), 1 );
							var endStrR = endR.getFullYear() + '-' + ( '0' + ( endR.getMonth() + 1 ) ).slice( -2 );
							$( '#appointment_month_to' ).val( endStrR );
						}
					}
				}

				// Ensure no duplicate totals are present before recalculation
				wp.cleanupPAOTotalsDuplicates();
				wp.scheduleCalculateCost();
				wp.validateForm();
				wp.updateInputHighlights();
			} );
			// Bind change events to add-on fields for cost calculation (scope to section to avoid duplicates)
			$addonsSection.off( 'change.cost' ).on( 'change.cost', 'input, select', function() {
				wp.scheduleCalculateCost();
				// Reflect required indicator promptly when fields toggle
				wp.updateAddonsHeaderRequiredIndicator();
			} );
			$addonsSection.off( 'change.pao' ).on( 'change.pao', '.wc-pao-addon-field', function() {
				$addonsSection.trigger( 'woocommerce-product-addons-update' );
				wp.updateAddonsHeaderRequiredIndicator();
			} );
			// Re-validate form after add-ons are loaded
			wp.validateForm();
		},

		// Toggle a required star in the Add-ons header when any required add-on field exists
		updateAddonsHeaderRequiredIndicator: function() {
			var $sec = $( '#wc-appointment-addons-section' );
			var $header = $( '#appointment_addons_link' );
			if ( !$header.length ) { return; }
			// Scope detection to the add-ons fields container to avoid matching the header itself
			var $body = $sec.find( '#wc-appointment-addons-fields, #wc-appointment-addons-container' );
			var hasRequired = false;
			try {
				$body.find( '[aria-required="true"], .wc-pao-addon-wrap.required, .wc-pao-required, input[required], select[required], textarea[required]' ).each( function() {
					hasRequired = true;
					return false; // break
				} );
			} catch ( e ) {}

			var $star = $header.find( '.required' );
			if ( hasRequired ) {
				if ( !$star.length ) {
					$header.append( ' <span class="required">*</span>' );
				}
			} else if ( $star.length ) {
				$star.remove();
			}
		},
		// ============================================================================
		// Section: Availability Responses & Suggestions
		// ----------------------------------------------------------------------------
		// Interprets availability AJAX responses and manages next-slot suggestion UI.
		// ============================================================================
		handleAvailabilityResponse: function( response, availKeyToCache ) {
			if ( response && response.success && response.data ) {
				if ( availKeyToCache ) {
					wp._availabilityCache[ availKeyToCache ] = {
						ts: Date.now(),
						response: response
					};
				}
				var available = !!response.data.available;
				wp.lastAvailability = available;
				if ( available ) {
					var cap = parseInt( response.data.capacity || 0, 10 );
					var msg = wc_appointment_modal_js_params.i18n_available;
					if ( 0 < cap ) {
						msg += ' (' + cap + ')';
					}
					wp.updateAvailabilityUI( 'success', msg );
				} else {
					var msg2 = wc_appointment_modal_js_params.i18n_unavailable;
					var next = response.data.next || response.data.next_same_start;
					if ( next && next.start ) {
						msg2 += ' — ' + wc_appointment_modal_js_params.i18n_next_available + ':';
					}
					wp.updateAvailabilityUI( 'error', msg2 );
					if ( next && next.start ) {
						wp.appendSuggestionLink( next );
					}
				}
			} else {
				wp.lastAvailability = null;
				wp.updateAvailabilityUI( 'error', wc_appointment_modal_js_params.i18n_error_checking );
			}
			// Leave submit button state controlled by validateForm (base fields + add-ons).
		},
		applyCombinedDurationToEnd: function() {
			var fromVal = wp.findInModal( '#appointment_time_from' ).val();
			if ( !fromVal ) {
				return;
			}
			var pd = wp.productData || {};
			var unit = pd.duration_unit || ( pd.has_time ? 'hour' : 'day' );
			if ( !( 'minute' === unit || 'hour' === unit ) ) {
				return;
			}
			var addMinutes = wp.getCombinedDurationMinutes( pd );
			var parts = fromVal.split( ':' );
			var startMin = ( parseInt( parts[ 0 ], 10 ) * 60 ) + parseInt( parts[ 1 ], 10 );
			var endMin = startMin + addMinutes;
			var norm = ( ( endMin % ( 24 * 60 ) ) + ( 24 * 60 ) ) % ( 24 * 60 );
			var endH = ( '0' + Math.floor( norm / 60 ) ).slice( -2 );
			var endM = ( '0' + ( norm % 60 ) ).slice( -2 );
			var toStr = endH + ':' + endM;
			wp.findInModal( '#appointment_time_to' ).val( toStr ).prop( 'disabled', false ).removeAttr( 'min' );
			if ( 'function' === typeof wp.populateToTimes ) {
				wp.populateToTimes( fromVal );
			}
			wp.validateForm();
			wp.scheduleCalculateCost();
			wp.updateInputHighlights();
		},

		// Build 'From' time suggestions for the day based on interval
		populateFromTimes: function( pd ) {
			wp.ensureTimeDatalists();
			// Use overlay-only suggestions (no datalist)
			// Keep full-day range on the input itself
			wp.findInModal( '#appointment_time_from' ).attr( 'min', '00:00' ).attr( 'max', '23:59' );
			var stepMin = wp.getIntervalMinutes( pd );
			var vals = [];
			for ( var m = 0; m < 24 * 60; m += stepMin ) {
				var h = ( '0' + Math.floor( m / 60 ) ).slice( -2 );
				var mm = ( '0' + ( m % 60 ) ).slice( -2 );
				vals.push( h + ':' + mm );
			}
			var curFrom = wp.findInModal( '#appointment_time_from' ).val();
			// Datalist disabled; overlay is the single source of suggestions
			// Render overlay suggestions under the input for better highlighting
			var $overlayFrom = wp.findInModal( '#appointment_time_from_suggestions' );
			if ( $overlayFrom.length ) {
				var ovHTML = '';
				for ( var j = 0; j < vals.length; j++ ) {
					var vv = vals[ j ];
					var selClass = ( vv === curFrom ) ? ' is-selected' : '';
					var optId = 'from-option-' + vv.replace( ':', '' );
					var ariaSel = selClass ? 'true' : 'false';
					ovHTML += '<div class="time-option' + selClass + '" role="option" tabindex="0" aria-selected="' + ariaSel + '" id="' + optId + '" data-value="' + vv + '">' +
						'<span class="time-value">' + wp.formatHHMMForDisplay( vv ) + '</span>' +
						( selClass ? '<span class="selected-mark">•</span>' : '' ) +
						'</div>';
				}
				$overlayFrom.html( ovHTML ).attr( 'aria-hidden', 'true' );
			}
		},

		// Build 'To' time suggestions from selected start up to midnight, include duration
		populateToTimes: function( fromVal ) {
			wp.ensureTimeDatalists();
			// Use overlay-only suggestions for 'To' (no datalist)
			if ( !fromVal ) {
				return;
			}
			var pd = wp.productData || {};
			var stepMin = wp.getIntervalMinutes( pd );
			var parts = fromVal.split( ':' );
			var startMin = ( parseInt( parts[ 0 ], 10 ) * 60 ) + parseInt( parts[ 1 ], 10 );
			const FULL_DAY_MIN = 24 * 60;

			// Insert a recommended end time based on combined duration
			var combined = wp.getCombinedDurationMinutes( pd );
			var curTo = wp.findInModal( '#appointment_time_to' ).val();
			var endStr = null;
			if ( 0 < combined ) {
				var endMinRec = startMin + combined;
				var dispRec = ( ( endMinRec % FULL_DAY_MIN ) + FULL_DAY_MIN ) % FULL_DAY_MIN;
				var endHRec = ( '0' + Math.floor( dispRec / 60 ) ).slice( -2 );
				var endMRec = ( '0' + ( dispRec % 60 ) ).slice( -2 );
				endStr = endHRec + ':' + endMRec;
				// Recommended end time calculated above; overlay handles display.
				// Datalist removed; recommended end time will appear in overlay with highlight
			}

			var vals = [];
			var diffs = [];
			// Generate options up to but not including +24h (wrap display past midnight)
			for ( var m = startMin + stepMin; m < startMin + FULL_DAY_MIN; m += stepMin ) {
				var disp = ( ( m % FULL_DAY_MIN ) + FULL_DAY_MIN ) % FULL_DAY_MIN;
				var h = ( '0' + Math.floor( disp / 60 ) ).slice( -2 );
				var mm = ( '0' + ( disp % 60 ) ).slice( -2 );
				vals.push( h + ':' + mm );
				diffs.push( m - startMin );
			}

			// Render overlay suggestions for 'To' time under the input
			var $overlayTo = wp.findInModal( '#appointment_time_to_suggestions' );
			if ( $overlayTo.length ) {
				var ovToHTML = '';
				for ( var i2 = 0; i2 < vals.length; i2++ ) {
					var v2 = vals[ i2 ];
					var classes = '';
					if ( v2 === curTo ) {
						classes += ' is-selected';
					}
					if ( endStr && v2 === endStr ) {
						classes += ' is-recommended';
					}
					var optId2 = 'to-option-' + v2.replace( ':', '' );
					var ariaSel2 = ( v2 === curTo ) ? 'true' : 'false';
					// Build compact duration label (e.g., "2h 30m")
					var diff2 = diffs[ i2 ];
					var hours2 = Math.floor( diff2 / 60 );
					var mins2 = diff2 % 60;
					var hShort = ( wc_appointment_modal_js_params.i18n_hours_short || 'h' );
					var mShort = ( wc_appointment_modal_js_params.i18n_minutes_short || 'm' );
					var labelParts2 = [];
					if ( 0 < hours2 ) {
						labelParts2.push( hours2 + hShort );
					}
					if ( 0 < mins2 ) {
						labelParts2.push( mins2 + mShort );
					}
					var durLabel2 = labelParts2.join( ' ' );
					ovToHTML += '<div class="time-option' + classes + '" role="option" tabindex="0" aria-selected="' + ariaSel2 + '" id="' + optId2 + '" data-value="' + v2 + '">' +
						'<span class="time-value">' + wp.formatHHMMForDisplay( v2 ) + '</span>' +
						( durLabel2 ? '<span class="time-duration"> (' + durLabel2 + ')</span>' : '' ) +
						( ( v2 === curTo ) ? '<span class="selected-mark">•</span>' : '' ) +
						'</div>';
				}
				$overlayTo.html( ovToHTML ).attr( 'aria-hidden', 'true' );
			}

			// Skip AJAX pretty labels; overlay already rendered
			// Pretty labels AJAX removed; overlay already provides localized labels
		},

		availabilityXHR: null,
		lastAvailability: null,
		availabilityTimer: null,

		// ============================================================================
		// Section: Formatting & Localization
		// ----------------------------------------------------------------------------
		// Formats date/time values for display and input, with safe Intl fallback.
		//
		// TIMEZONE ARCHITECTURE DOCUMENTATION:
		// ====================================
		// This system uses a specific timezone handling approach that is critical to understand:
		//
		// 1. TIMESTAMP STORAGE:
		//    - Timestamps are stored as UTC Unix timestamps (seconds since epoch)
		//    - However, these UTC timestamps REPRESENT times in the site's configured timezone
		//    - Example: If site timezone is "Europe/Ljubljana" (UTC+1), a timestamp representing
		//      "2024-01-15 14:00" in Ljubljana time is stored as a UTC timestamp that, when
		//      interpreted as UTC, would show "2024-01-15 13:00 UTC", but we treat it as
		//      "2024-01-15 14:00" in site timezone
		//
		// 2. EXTRACTION PRINCIPLE:
		//    - When extracting time components from timestamps, we use LOCAL methods
		//      (getHours(), getDate(), etc.) NOT UTC methods (getUTCHours(), getUTCDate())
		//    - This is because the timestamp already represents the site timezone time
		//    - Using UTC methods would incorrectly shift the time by the timezone offset
		//
		// 3. NO TIMEZONE CONVERSION:
		//    - The selected time is already UTC (representing site timezone)
		//    - We do NOT perform timezone conversion when extracting components
		//    - We do NOT adjust timestamps for timezone differences
		//
		// 4. CONVERSION TO REAL UTC:
		//    - When timestamps need to be converted to actual UTC (for API calls, database, etc.),
		//      this conversion happens on the server side, not in this JavaScript code
		//    - The server knows the site timezone and can properly convert
		//
		// 5. DISPLAY FORMATTING:
		//    - For display purposes, we use Intl API with site_timezone parameter when available
		//    - For input fields, we extract components directly using local methods
		//
		// CRITICAL: Always use local methods (getHours, getDate) when extracting from timestamps
		//           that represent site timezone times. Never use UTC methods (getUTCHours, etc.)
		//           as this will cause time shifts and incorrect times (e.g., showing 23:00
		//           for the previous day when it should be 00:00 for the selected day).
		// ============================================================================
		/**
		 * Format timestamp as date/time string for display
		 * Extracts components using _formatPartsLocal (local methods, no timezone conversion)
		 * since timestamp represents site timezone time
		 */
		formatDateTime: function( ts ) {
			if ( !ts ) {
				return '';
			}
			var p = wp._formatPartsLocal( ts );
			try {
				// Use a Date object to format the date/time using the extracted parts (already in site timezone)
				// Components were extracted using local methods, so they represent site timezone time
				var d = new Date( parseInt( p.year, 10 ), parseInt( p.month, 10 ) - 1, parseInt( p.day, 10 ), parseInt( p.hour, 10 ), parseInt( p.minute, 10 ) );
				return d.toLocaleString( undefined, {
					year: 'numeric',
					month: 'short',
					day: 'numeric',
					hour: '2-digit',
					minute: '2-digit'
				} );
			} catch ( e ) {
				return p.year + '-' + p.month + '-' + p.day + ' ' + p.hour + ':' + p.minute;
			}
		},

		/**
		 * Date-only formatter for daily products
		 * Uses Intl API with site_timezone for proper display formatting
		 * Note: Timestamp represents site timezone time, but Intl API handles conversion
		 */
		formatDate: function( ts ) {
			if ( !ts ) {
				return '';
			}
			var d = new Date( parseInt( ts, 10 ) * 1000 );
			var tz = ( window.wc_appointment_modal_js_params && wc_appointment_modal_js_params.site_timezone ) ? wc_appointment_modal_js_params.site_timezone : undefined;
			try {
				return d.toLocaleDateString( undefined, {
					year: 'numeric',
					month: 'short',
					day: 'numeric',
					timeZone: tz
				} );
			} catch ( e ) {
				// Fallback: Use local methods (timestamp represents site timezone time)
				return d.getFullYear() + '-' + ( '0' + ( d.getMonth() + 1 ) ).slice( -2 ) + '-' + ( '0' + d.getDate() ).slice( -2 );
			}
		},

		/**
		 * Format timestamp as time string for display
		 * Extracts components using _formatPartsLocal (local methods, no timezone conversion)
		 * since timestamp represents site timezone time
		 */
		formatTime: function( ts ) {
			if ( !ts ) {
				return '';
			}
			var p = wp._formatPartsLocal( ts );
			try {
				// Use a Date object to format the time using the extracted parts (already in site timezone)
				// Components were extracted using local methods, so they represent site timezone time
				var d = new Date( 2000, 0, 1, parseInt( p.hour, 10 ), parseInt( p.minute, 10 ) );
				return d.toLocaleTimeString( undefined, {
					hour: '2-digit',
					minute: '2-digit'
				} );
			} catch ( e ) {
				return p.hour + ':' + p.minute;
			}
		},

		// Format a 'HH:MM' string for display to match input's locale
		formatHHMMForDisplay: function( hhmm ) {
			if ( !hhmm ) {
				return '';
			}
			var parts = ( hhmm + '' ).split( ':' );
			if ( 2 > parts.length ) {
				return hhmm;
			}
			var h = parseInt( parts[ 0 ], 10 );
			var m = parseInt( parts[ 1 ], 10 );
			if ( isNaN( h ) || isNaN( m ) ) {
				return hhmm;
			}
			try {
				var d = new Date( 2000, 0, 1, h, m, 0, 0 );
				return d.toLocaleTimeString( undefined, {
					hour: '2-digit',
					minute: '2-digit'
				} );
			} catch ( e ) {
				return ( '0' + h ).slice( -2 ) + ':' + ( '0' + m ).slice( -2 );
			}
		},

		// Format a date string (YYYY-MM-DD) for display to match modal format
		formatDateForDisplay: function( dateStr ) {
			if ( !dateStr ) {
				return dateStr;
			}
			try {
				var parts = dateStr.split( '-' );
				if ( 3 === parts.length ) {
					var year = parseInt( parts[ 0 ], 10 );
					var month = parseInt( parts[ 1 ], 10 ) - 1; // JS months are 0-indexed
					var day = parseInt( parts[ 2 ], 10 );
					var date = new Date( year, month, day );
					var tz = wp._getTZ();
					return date.toLocaleDateString( undefined, {
						year: 'numeric',
						month: 'short',
						day: 'numeric',
						timeZone: tz
					} );
				}
			} catch ( e ) {}
			return dateStr;
		},

		// Format a month string (YYYY-MM) for display to match modal format
		formatMonthForDisplay: function( monthStr ) {
			if ( !monthStr ) {
				return monthStr;
			}
			try {
				var parts = monthStr.split( '-' );
				if ( 2 === parts.length ) {
					var year = parseInt( parts[ 0 ], 10 );
					var month = parseInt( parts[ 1 ], 10 ) - 1;
					var date = new Date( year, month, 1 );
					var tz = wp._getTZ();
					return date.toLocaleDateString( undefined, {
						year: 'numeric',
						month: 'short',
						timeZone: tz
					} );
				}
			} catch ( e ) {}
			return monthStr;
		},

		// Helpers: build input-friendly values in site timezone
		_getTZ: function() {
			return ( window.wc_appointment_modal_js_params && wc_appointment_modal_js_params.site_timezone ) ? wc_appointment_modal_js_params.site_timezone : undefined;
		},
		/**
		 * Format parts treating timestamp as already in site timezone (no conversion)
		 *
		 * CRITICAL TIMEZONE HANDLING:
		 * ===========================
		 * This function extracts date/time components from a UTC timestamp that represents
		 * a time in the site's configured timezone. The timestamp is stored as UTC but
		 * semantically represents site timezone time.
		 *
		 * IMPORTANT: We use LOCAL methods (getHours, getDate, etc.) NOT UTC methods
		 * (getUTCHours, getUTCDate, etc.) because:
		 *
		 * 1. The timestamp already represents the correct site timezone time
		 * 2. Using UTC methods would incorrectly shift the time by the timezone offset
		 * 3. Example: A timestamp representing "2024-01-15 00:00" in site timezone
		 *    should extract as 00:00, not 23:00 (which would happen with UTC methods
		 *    if site is UTC+1)
		 *
		 * @param {number|string} ts - Unix timestamp in seconds (UTC, but represents site timezone time)
		 * @returns {Object} Object with year, month, day, hour, minute as strings
		 */
		_formatPartsLocal: function( ts ) {
			var d = new Date( parseInt( ts, 10 ) * 1000 );
			// CRITICAL: Use local methods (getHours, getDate) NOT UTC methods (getUTCHours, getUTCDate)
			// The timestamp represents site timezone time, so we extract components directly
			// without timezone conversion. This prevents time shifts (e.g., 23:00 showing for previous day).
			return {
				year: '' + d.getFullYear(),
				month: ( '0' + ( d.getMonth() + 1 ) ).slice( -2 ),
				day: ( '0' + d.getDate() ).slice( -2 ),
				hour: ( '0' + d.getHours() ).slice( -2 ),
				minute: ( '0' + d.getMinutes() ).slice( -2 )
			};
		},
		/**
		 * Format timestamp as date string (YYYY-MM-DD) for input fields
		 * Uses _formatPartsLocal which extracts components using local methods
		 * (timestamp represents site timezone time, no conversion needed)
		 */
		formatInputDate: function( ts ) {
			var p = wp._formatPartsLocal( ts );
			return p.year + '-' + p.month + '-' + p.day;
		},
		/**
		 * Format timestamp as time string (HH:MM) for input fields
		 * Uses _formatPartsLocal which extracts components using local methods
		 * (timestamp represents site timezone time, no conversion needed)
		 */
		formatInputTime: function( ts ) {
			var p = wp._formatPartsLocal( ts );
			return p.hour + ':' + p.minute;
		},
		/**
		 * Format timestamp as month string (YYYY-MM) for input fields
		 * Uses _formatPartsLocal which extracts components using local methods
		 * (timestamp represents site timezone time, no conversion needed)
		 */
		formatInputMonth: function( ts ) {
			var p = wp._formatPartsLocal( ts );
			return p.year + '-' + p.month;
		},

		updateAvailabilityUI: function( type, message ) {
			var $wrap = wp.findInModal( '#appointment-availability-wrap' );
			var $status = wp.findInModal( '#appointment-availability-status' );
			$wrap.show();
			$status.removeClass( 'notice-success notice-error notice-info' );
			if ( 'success' === type ) {
				$status.addClass( 'notice notice-success' );
			} else if ( 'error' === type ) {
				$status.addClass( 'notice notice-error' );
			} else {
				$status.addClass( 'notice notice-info' );
			}

			// Build context details without labels
			var ctxParts = [];

			// Prefer actual values over visibility checks to avoid stale UI context
			var d = wp.findInModal( '#appointment_date' ).val();
			var tf = wp.findInModal( '#appointment_time_from' ).val();
			var tt = wp.findInModal( '#appointment_time_to' ).val();
			var df = wp.findInModal( '#appointment_date_from' ).val();
			var dt = wp.findInModal( '#appointment_date_to' ).val();
			var mf = wp.findInModal( '#appointment_month_from' ).val();
			var mt = wp.findInModal( '#appointment_month_to' ).val();

			if ( d ) {
				var formattedDate = wp.formatDateForDisplay( d );
				if ( tf && tt ) {
					var formattedTimeFrom = wp.formatHHMMForDisplay( tf );
					var formattedTimeTo = wp.formatHHMMForDisplay( tt );
					ctxParts.push( formattedDate + ' ' + formattedTimeFrom + '–' + formattedTimeTo );
				} else {
					ctxParts.push( formattedDate );
				}
			} else if ( df ) {
				var formattedDateFrom = wp.formatDateForDisplay( df );
				var formattedDateTo = dt && dt !== df ? wp.formatDateForDisplay( dt ) : '';
				ctxParts.push( formattedDateTo ? ( formattedDateFrom + ' → ' + formattedDateTo ) : formattedDateFrom );
			} else if ( mf ) {
				var formattedMonthFrom = wp.formatMonthForDisplay( mf );
				var formattedMonthTo = mt && mt !== mf ? wp.formatMonthForDisplay( mt ) : '';
				ctxParts.push( formattedMonthTo ? ( formattedMonthFrom + ' → ' + formattedMonthTo ) : formattedMonthFrom );
			}

			if ( $( '#appointment_staff_id' ).is( ':visible' ) ) {
				var staffLabel = $( '#appointment_staff_id option:selected' ).text().trim();
				if ( staffLabel ) {
					ctxParts.push( staffLabel );
				}
			}

			var finalMsg = ( ctxParts.length ? ctxParts.join( ' \u2022 ' ) + ' — ' : '' ) + message;
			$status.text( finalMsg );
		},

		// Append clickable Next Available link to the status notice
		appendSuggestionLink: function( next ) {
			var $status = wp.findInModal( '#appointment-availability-status' );
			if ( !next || !next.start ) {
				return;
			}
			var showTimeRange = wp.findInModal( '.wc-appointment-time-range' ).is( ':visible' );
			var showDateRange = wp.findInModal( '.wc-appointment-date-range' ).is( ':visible' );
			var showMonthRange = wp.findInModal( '.wc-appointment-month-range' ).is( ':visible' );
			var text;
			if ( showTimeRange ) {
				var startStr = wp.formatDateTime( next.start );
				var endStr = next.end ? wp.formatTime( next.end ) : '';
				text = endStr ? ( startStr + '–' + endStr ) : startStr;
			} else if ( showDateRange || showMonthRange ) {
				var s = wp.formatDate( next.start );
				var e = '';
				if ( next.end ) {
					var endTsDisplay;
					if ( showMonthRange ) {
						// Suggestion end is boundary at first day of next month; display inclusive label by subtracting one month.
						endTsDisplay = ( function( t ) { var d = new Date( t * 1000 ); d.setMonth( d.getMonth() - 1 ); return Math.floor( d.getTime() / 1000 ); } )( next.end );
					} else {
						// Suggestion end is boundary at start of next day; display inclusive label by subtracting one day.
						endTsDisplay = ( function( t ) { var d = new Date( t * 1000 ); d.setDate( d.getDate() - 1 ); return Math.floor( d.getTime() / 1000 ); } )( next.end );
					}
					e = wp.formatDate( endTsDisplay );
				}
				text = ( e && e !== s ) ? ( s + ' → ' + e ) : s;
			} else {
				text = wp.formatDate( next.start );
			}
			var $link = $( '<a href="#" id="appointment-next-suggestion" class="appointment-next-suggestion"></a>' ).text( text );
			$link.attr( 'data-start', parseInt( next.start, 10 ) );
			if ( next.end ) {
				$link.attr( 'data-end', parseInt( next.end, 10 ) );
			}
			$status.append( ' ' );
			$status.append( $link );
		},

		// Handle suggestion click to auto-fill inputs and revalidate
		onNextSuggestionClick: function( e ) {
			e.preventDefault();
			var $a = $( this );
			var tsStart = parseInt( $a.attr( 'data-start' ), 10 ) || 0;
			var tsEnd = parseInt( $a.attr( 'data-end' ) || '0', 10 ) || 0;
			if ( !tsStart ) { return; }
			wp.prefillSelectedRange( tsStart, tsEnd );
		},

		/**
		 * Prefill form fields with selected date/time range from calendar or suggestions
		 *
		 * TIMEZONE NOTE:
		 * ==============
		 * Input timestamps (tsStart, tsEnd) are UTC timestamps representing site timezone times.
		 * We use formatInputDate/formatInputTime/formatInputMonth which internally call
		 * _formatPartsLocal to extract components using local methods (no timezone conversion).
		 * This ensures the displayed date/time matches what the timestamp represents in site timezone.
		 *
		 * @param {number|string} tsStart - Start timestamp (UTC, represents site timezone time)
		 * @param {number|string} tsEnd - End timestamp (UTC, represents site timezone time)
		 */
		prefillSelectedRange: function( tsStart, tsEnd ) {
			// Remember the prefill so init can honor it and not overwrite
			try { wp._prefillRange = { startTs: parseInt( tsStart || '0', 10 ) || 0, endTs: parseInt( tsEnd || '0', 10 ) || 0 }; } catch ( e0 ) { wp._prefillRange = { startTs: 0, endTs: 0 }; }
			try { tsStart = parseInt( tsStart, 10 ) || 0; } catch ( e ) { tsStart = 0; }
			try { tsEnd = parseInt( tsEnd || '0', 10 ) || 0; } catch ( e2 ) { tsEnd = 0; }
			if ( !tsStart ) { return; }
			function applyOnce() {
				var showSingleDate = wp.findInModal( '.wc-appointment-date-field' ).is( ':visible' );
				var showTimeRange = wp.findInModal( '.wc-appointment-time-range' ).is( ':visible' );
				var showDateRange = wp.findInModal( '.wc-appointment-date-range' ).is( ':visible' );
				var showMonthRange = wp.findInModal( '.wc-appointment-month-range' ).is( ':visible' );
				if ( showMonthRange ) {
					wp.findInModal( '#appointment_month_from' ).val( wp.formatInputMonth( tsStart ) );
					var endMonthTs = tsEnd ? ( function( t ) { var d = new Date( t * 1000 ); d.setMonth( d.getMonth() - 1 ); return Math.floor( d.getTime() / 1000 ); } )( tsEnd ) : ( function( t ) { var pdM = wp.productData || {}; var baseMonths = parseInt( pdM.duration || 1, 10 ); var addonMonths = parseInt( wp.findInModal( '#wc_appointments_field_addons_duration' ).val() || '0', 10 ); var months = baseMonths + ( isNaN( addonMonths ) ? 0 : addonMonths ); var d0 = new Date( t * 1000 ); d0.setMonth( d0.getMonth() + Math.max( months - 1, 0 ) ); return Math.floor( d0.getTime() / 1000 ); } )( tsStart );
					wp.findInModal( '#appointment_month_to' ).val( wp.formatInputMonth( endMonthTs ) );
				} else if ( showDateRange ) {
					wp.findInModal( '#appointment_date_from' ).val( wp.formatInputDate( tsStart ) );
					var endDateTs = tsEnd ? ( function( t ) { var d = new Date( t * 1000 ); d.setDate( d.getDate() - 1 ); return Math.floor( d.getTime() / 1000 ); } )( tsEnd ) : ( function( t ) { var pdD = wp.productData || {}; var baseDays = parseInt( pdD.duration || 1, 10 ); var addonDays = parseInt( wp.findInModal( '#wc_appointments_field_addons_duration' ).val() || '0', 10 ); var days = baseDays + ( isNaN( addonDays ) ? 0 : addonDays ); var endExclusive = t + ( Math.max( days, 1 ) * 86400 ); return endExclusive - 86400; } )( tsStart );
					wp.findInModal( '#appointment_date_to' ).val( wp.formatInputDate( endDateTs ) );
				} else if ( showSingleDate ) {
					wp.findInModal( '#appointment_date' ).val( wp.formatInputDate( tsStart ) );
					if ( showTimeRange ) {
						wp.findInModal( '#appointment_time_from' ).val( wp.formatInputTime( tsStart ) ).prop( 'disabled', false );
						var isAllDayPrefillLocal = !!tsEnd && ( 86399 <= ( tsEnd - tsStart ) );
						if ( tsEnd && !isAllDayPrefillLocal ) {
							wp.findInModal( '#appointment_time_to' ).val( wp.formatInputTime( tsEnd ) ).prop( 'disabled', false );
						} else {
							wp.applyCombinedDurationToEnd();
						}
						if ( 'function' === typeof wp.populateToTimes ) {
							wp.populateToTimes( wp.findInModal( '#appointment_time_from' ).val() );
						}
					}
				} else {
					return false;
				}
				wp.updateInputHighlights();
				wp.validateForm();
				wp.scheduleCalculateCost();
				wp.maybeCheckAvailability();
				return true;
			}
			if ( applyOnce() ) { return; }
			var tries = 0; var maxTries = 40; var iv = setInterval( function() {
				tries++;
				if ( applyOnce() ) { clearInterval( iv ); return; }
				if ( tries >= maxTries ) { clearInterval( iv ); }
			}, 100 );
		},

		// Build a key representing current availability parameters.
		// Key is used to prevent redundant availability AJAX calls.
		computeAvailabilityKey: function() {
			var productId = wp.findInModal( '#appointment_product_id' ).val();
			if ( !productId ) {
				return null; // missing product
			}
			var data = {
				appointment_product_id: productId,
				appointment_staff_id: wp.findInModal( '#appointment_staff_id' ).val() || ''
			};
			// Include selected add-on duration (minutes)
			data.duration = wp.getAddonDurationMinutes();
			// Include quantity
			var qty = parseInt( wp.findInModal( '#appointment_qty' ).val() || '1', 10 );
			data.quantity = qty;

			// Prefer range values first
			var d  = wp.findInModal( '#appointment_date' ).val();
			var tf = wp.findInModal( '#appointment_time_from' ).val();
			var tt = wp.findInModal( '#appointment_time_to' ).val();
			var df = wp.findInModal( '#appointment_date_from' ).val();
			var dt = wp.findInModal( '#appointment_date_to' ).val();
			var mf = wp.findInModal( '#appointment_month_from' ).val();
			var mt = wp.findInModal( '#appointment_month_to' ).val();

			if ( df && dt ) {
				data.appointment_date_from = df;
				data.appointment_date_to = dt;
			} else if ( mf && mt ) {
				data.appointment_month_from = mf;
				data.appointment_month_to = mt;
			} else if ( d ) {
				data.appointment_date = d;
				if ( tf && tt ) {
					data.appointment_time_from = tf;
					data.appointment_time_to = tt;
				}
			} else {
				return null; // insufficient params
			}
			// Ask server to prefer same start time as 'from'
			if ( data.appointment_time_from ) {
				data.prefer_same_start_time = '1';
			}
			return wp.buildAvailabilityKey( productId, data );
		},

		// ============================================================================
		// Section: Availability Checking
		// ----------------------------------------------------------------------------
		// Computes a key for selected params, caches responses, and suggests next slot.
		// ============================================================================
		// Only run availability when the computed key changes.
		maybeCheckAvailability: function() {
			try {
				var key = wp.computeAvailabilityKey();
				if ( !key ) {
					return; // not enough info to check
				}
				if ( key !== wp._lastAvailabilityKey ) {
					wp._lastAvailabilityKey = key;
					wp.checkAvailability();
				}
			} catch ( e ) {}
		},

		checkAvailability: function() {
			// Always scope reads to the modal to avoid picking up stale fields elsewhere in the admin
			var productId = wp.findInModal( '#appointment_product_id' ).val();
			if ( !productId ) {
				wp.lastAvailability = null;
				return;
			}

			var data = {
				action: 'wc_appointments_check_availability',
				nonce: wc_appointment_modal_js_params.nonce_check_availability,
				appointment_product_id: productId,
				appointment_staff_id: wp.findInModal( '#appointment_staff_id' ).val() || ''
			};
			// Include selected add-on duration (minutes) so server can consider combined duration
			data.duration = wp.getAddonDurationMinutes();
			// Include quantity selected in the modal
			var qty = parseInt( wp.findInModal( '#appointment_qty' ).val() || '1', 10 );
			data.quantity = qty;

			// Prefer range values first to avoid picking a stray single-date field
			var d  = wp.findInModal( '#appointment_date' ).val();
			var tf = wp.findInModal( '#appointment_time_from' ).val();
			var tt = wp.findInModal( '#appointment_time_to' ).val();
			var df = wp.findInModal( '#appointment_date_from' ).val();
			var dt = wp.findInModal( '#appointment_date_to' ).val();
			var mf = wp.findInModal( '#appointment_month_from' ).val();
			var mt = wp.findInModal( '#appointment_month_to' ).val();

			if ( df && dt ) {
				data.appointment_date_from = df;
				data.appointment_date_to = dt;
			} else if ( mf && mt ) {
				data.appointment_month_from = mf;
				data.appointment_month_to = mt;
			} else if ( d ) {
				data.appointment_date = d;
				if ( tf && tt ) {
					data.appointment_time_from = tf;
					data.appointment_time_to = tt;
				}
			} else {
				return;
			}

			// Ask server to prefer a next suggestion with same start time as 'from'
			if ( data.appointment_time_from ) {
				data.prefer_same_start_time = '1';
			}

			// Build a cache key for availability based on current selections
			var availKey = wp.buildAvailabilityKey( productId, data );
			var nowTs = Date.now();
			var cachedAvail = wp._availabilityCache[ availKey ];
			if ( cachedAvail && ( nowTs - cachedAvail.ts ) < ( wp._availabilityCacheTTL || 5000 ) ) {
				wp.handleAvailabilityResponse( cachedAvail.response, null );
				return;
			}

			if ( wp.availabilityTimer ) {
				clearTimeout( wp.availabilityTimer );
			}
			wp.availabilityTimer = setTimeout( function() {
				if ( wp.availabilityXHR && wp.availabilityXHR.abort ) {
					wp.availabilityXHR.abort();
				}

				wp.updateAvailabilityUI( 'info', wc_appointment_modal_js_params.i18n_checking_availability );

				wp.showLoading();
				wp.availabilityXHR = $.post( wc_appointment_modal_js_params.ajax_url, data )
					.done( function( response ) {
						wp.handleAvailabilityResponse( response, availKey );
					} )
					.fail( function() {
						wp.handleAvailabilityResponse( null, availKey );
					} ).always( function() {
						wp.hideLoading();
					} );
			}, 300 );
		},

		onOrderTypeChange: function() {
			var orderType = $( '#order_type' ).val();
			if ( 'existing' === orderType ) {
				$( '.wc-appointment-existing-order' ).show();
				$( '.wc-appointment-billing-details' ).hide();
				if ( !$( '#existing_order_id' ).hasClass( 'select2-hidden-accessible' ) && !$( '#existing_order_id' ).hasClass( 'selectWoo-hidden-accessible' ) ) {
					wp.initOrderSearch();
				}
			} else {
				$( '.wc-appointment-existing-order' ).hide();
				$( '.wc-appointment-billing-details' ).show();
			}
			wp.validateForm();
		},

		onBillingCountryChange: function() {
			var $country = $( '#_billing_country' );
			var $state = $( '#_billing_state' );
			var countries = {};
			try {
				countries = JSON.parse( wc_appointment_modal_js_params.billing.countries || '{}' );
			} catch ( e ) {}
			var code = $country.val();
			var states = countries[ code ] || {};
			$state.empty();
			if ( states && Object.keys( states ).length ) {
				var placeholder = ( wc_appointment_modal_js_params.i18n && wc_appointment_modal_js_params.i18n.select_state ) ? wc_appointment_modal_js_params.i18n.select_state : 'Select a state / county…';
				$state.append( '<option value="">' + placeholder + '</option>' );
				$.each( states, function( k, v ) {
					$state.append( '<option value="' + k + '">' + v + '</option>' );
				} );
				$state.prop( 'disabled', false );
			} else {
				$state.append( '<option value="">' + wc_appointment_modal_js_params.billing.default_state + '</option>' );
				$state.prop( 'disabled', true );
			}
			if ( $.fn.selectWoo && $state.hasClass( 'selectWoo-hidden-accessible' ) ) {
				$state.selectWoo( 'destroy' ).selectWoo();
			} else if ( $.fn.select2 && $state.hasClass( 'select2-hidden-accessible' ) ) {
				$state.select2( 'destroy' ).select2();
			}
			wp.validateForm();
		},

		onCustomerSelected: function() {
			var userId = $( '#customer_user' ).val();
			if ( !userId ) {
				return;
			}
			wp.showLoading();
			$.ajax( {
				url: wc_appointment_modal_js_params.ajax_url,
				type: 'POST',
				data: {
					action: 'wc_appointments_get_customer_details',
					user_id: userId,
					security: wc_appointment_modal_js_params.nonce_get_customer_details
				}
			} ).done( function( res ) {
				if ( res && res.success && res.data ) {
					$( '#_billing_first_name' ).val( res.data.first_name || '' );
					$( '#_billing_last_name' ).val( res.data.last_name || '' );
					$( '#_billing_company' ).val( res.data.company || '' );
					$( '#_billing_address_1' ).val( res.data.address_1 || '' );
					$( '#_billing_address_2' ).val( res.data.address_2 || '' );
					$( '#_billing_city' ).val( res.data.city || '' );
					$( '#_billing_postcode' ).val( res.data.postcode || '' );
					if ( res.data.country ) {
						$( '#_billing_country' ).val( res.data.country ).trigger( 'change' );
					}
					setTimeout( function() {
						var state = res.data.state || '';
						var $state = $( '#_billing_state' );
						if ( state && $state.find( 'option[value="' + state + '"]' ).length ) {
							$state.val( state ).trigger( 'change' );
						} else {
							$state.val( '' ).trigger( 'change' );
						}
					}, 60 );
					$( '#_billing_email' ).val( res.data.email || '' );
					$( '#_billing_phone' ).val( res.data.phone || '' );
					// Do not alter the selected customer's label; keep exactly what was selected
					$( '.wc-appointment-billing-details' ).removeClass( 'collapsed' );
					wp.validateForm();
				}
			} ).fail( function( xhr, status, err ) {
				console.log( 'Get customer details failed:', status, err );
			} ).always( function() {
				wp.hideLoading();
			} );
		},

		toggleBillingSection: function( e ) {
			if ( e ) {
				e.preventDefault();
			}
			$( '.wc-appointment-billing-details' ).toggleClass( 'collapsed' );
		},

		// Mirror Billing: toggle Add-ons collapse
		toggleAddonsSection: function( e ) {
			if ( e ) {
				e.preventDefault();
			}
			$( '.wc-appointment-addons' ).toggleClass( 'collapsed' );
		},

		// ============================================================================
		// Section: Pricing & Overrides
		// ----------------------------------------------------------------------------
		// Calculates price, handles manual overrides, and updates footer display.
		// ============================================================================
		onPriceEditClick: function() {
			var $input = $( '#appointment_override_price' );
			var $display = $( '#appointment_footer_price' );
			var current = $input.val();
			if ( !current ) {
				var raw = $( '#appointment_calculated_price_raw' ).val();
				current = raw || ( $display.text() || '' ).replace( /[^\d.]/g, '' );
			}
			$input.val( current ).show().focus().select();
			$( '#appointment_price_edit' ).hide();
		},

		onPriceEditCommit: function() {
			var val = $( '#appointment_override_price' ).val();
			if ( '' === val ) {
				$( '#appointment_override_price_hidden' ).val( '' );
				$( '#appointment_override_price' ).hide();
				$( '#appointment_price_edit' ).show();
				// Refresh footer with calculated/base amount
				wp.scheduleCalculateCost();
				return;
			}
			var num = parseFloat( ( '' + val ).replace( ',', '.' ) );
			if ( !isNaN( num ) ) {
				$( '#appointment_override_price_hidden' ).val( num );
				var html = wp.formatPriceHTML( num );
				if ( html ) {
					$( '#appointment_footer_price' ).html( html );
				}
			} else {
				$( '#appointment_override_price_hidden' ).val( val );
			}
			$( '#appointment_override_price' ).hide();
			$( '#appointment_price_edit' ).show();
		},

		onPriceEditKey: function( e ) {
			if ( 'Enter' === e.key ) {
				wp.onPriceEditCommit();
			} else if ( 'Escape' === e.key ) {
				$( '#appointment_override_price_hidden' ).val( '' );
				$( '#appointment_override_price' ).val( '' ).hide();
				$( '#appointment_footer_price' ).show();
				$( '#appointment_price_edit' ).show();
				wp.scheduleCalculateCost();
			}
		},

		initDatePicker: function( productData ) {
			var unit = productData && productData.duration_unit ? productData.duration_unit : ( productData && productData.has_time ? 'hour' : 'day' );
			var today = new Date();
			var yyyy = today.getFullYear();
			var mm = ( '0' + ( today.getMonth() + 1 ) ).slice( -2 );

			wp.findInModal( '#appointment_date, #appointment_date_from, #appointment_date_to' ).val( '' );
			wp.findInModal( '#appointment_month_from, #appointment_month_to' ).val( '' );
			wp.findInModal( '#appointment_time_from, #appointment_time_to' ).val( '' );
			wp.findInModal( '#appointment_time_to' ).prop( 'disabled', true ).attr( 'min', '' );

			if ( 'minute' === unit || 'hour' === unit ) {
				// Allow selecting past dates in admin; do not set HTML min on date
				wp.findInModal( '#appointment_time_from, #appointment_time_to' ).prop( 'disabled', true );
				if ( 'function' === typeof wp.ensureTimeDatalists ) {
					wp.ensureTimeDatalists();
				}
				if ( 'function' === typeof wp.populateFromTimes ) {
					wp.populateFromTimes( productData );
				}

				// If a range was selected in calendar, prefer it and skip defaults
				if ( wp._prefillRange && wp._prefillRange.startTs ) {
					wp.prefillSelectedRange( wp._prefillRange.startTs, wp._prefillRange.endTs );
				} else if ( productData ) {
					var next = new Date();
					next.setMinutes( 0, 0, 0 );
					next.setHours( next.getHours() + 1 );
					var duration = parseInt( productData.duration || 0, 10 );
					var minutesDelta = duration ? ( 'hour' === unit ? ( duration * 60 ) : duration ) : 60;
					var end = new Date( next.getTime() );
					end.setMinutes( end.getMinutes() + minutesDelta );
					var nextDateStr = next.getFullYear() + '-' + ( '0' + ( next.getMonth() + 1 ) ).slice( -2 ) + '-' + ( '0' + next.getDate() ).slice( -2 );
					var fromTimeStr = ( '0' + next.getHours() ).slice( -2 ) + ':' + ( '0' + next.getMinutes() ).slice( -2 );
					var toTimeStr = ( '0' + end.getHours() ).slice( -2 ) + ':' + ( '0' + end.getMinutes() ).slice( -2 );
					wp.findInModal( '#appointment_date' ).val( nextDateStr );
					wp.findInModal( '#appointment_time_from' ).val( fromTimeStr ).prop( 'disabled', false );
					wp.findInModal( '#appointment_time_to' ).val( toTimeStr ).prop( 'disabled', false ).attr( 'min', fromTimeStr );
					if ( 'function' === typeof wp.populateToTimes ) {
						wp.populateToTimes( fromTimeStr );
					}
				}
			} else if ( 'day' === unit ) {
				// Do not restrict past days; remove HTML min for date range inputs
				wp.findInModal( '#appointment_date_from' ).off( 'change' ).on( 'change', function() {
					wp.findInModal( '#appointment_date_to' ).attr( 'min', $( this ).val() );
					// Auto-fill "to" date using combined duration in days (product + add-ons).
					const pd = wp.productData || productData || {};
					var baseDays = parseInt( pd.duration || 1, 10 );
					var addonDays = parseInt( wp.findInModal( '#wc_appointments_field_addons_duration' ).val() || '0', 10 );
					var days = baseDays + ( isNaN( addonDays ) ? 0 : addonDays );
					var fromVal = $( this ).val();
					if ( fromVal ) {
						// TIMEZONE NOTE: Date string (YYYY-MM-DD) is treated as local date in site timezone
						// When converted to Date object, it represents that date at midnight in browser's
						// local timezone. This is correct because the date string represents a site timezone date.
						var df = new Date( fromVal.replace( /-/g, '/' ) );
						df.setDate( df.getDate() + Math.max( days - 1, 0 ) );
						var y = df.getFullYear();
						var m = ( '0' + ( df.getMonth() + 1 ) ).slice( -2 );
						var d = ( '0' + df.getDate() ).slice( -2 );
						wp.findInModal( '#appointment_date_to' ).val( y + '-' + m + '-' + d );
					}
					// Delegate downstream updates to unified handler to avoid duplicate availability checks
					wp.onRangeChange();
				} );
				// Do not set HTML min on "to" date to allow past selections
				// If a range was selected in calendar, prefer it and skip defaults
				if ( wp._prefillRange && wp._prefillRange.startTs ) {
					wp.prefillSelectedRange( wp._prefillRange.startTs, wp._prefillRange.endTs );
				} else if ( productData ) {
					const pd = wp.productData || productData || {};
					var baseDays = parseInt( pd.duration || 1, 10 );
					var addonDays = parseInt( wp.findInModal( '#wc_appointments_field_addons_duration' ).val() || '0', 10 );
					var days = baseDays + ( isNaN( addonDays ) ? 0 : addonDays );
					var from = new Date( today.getTime() );
					var to = new Date( today.getTime() );
					to.setDate( to.getDate() + Math.max( days - 1, 0 ) );
					var fromDateStr = from.getFullYear() + '-' + ( '0' + ( from.getMonth() + 1 ) ).slice( -2 ) + '-' + ( '0' + from.getDate() ).slice( -2 );
					var toDateStr = to.getFullYear() + '-' + ( '0' + ( to.getMonth() + 1 ) ).slice( -2 ) + '-' + ( '0' + to.getDate() ).slice( -2 );
					wp.findInModal( '#appointment_date_from' ).val( fromDateStr );
					wp.findInModal( '#appointment_date_to' ).val( toDateStr );
				}
			} else if ( 'month' === unit ) {
				var ym = yyyy + '-' + mm;
				wp.findInModal( '#appointment_month_from' ).attr( 'min', ym ).off( 'change' ).on( 'change', function() {
					wp.findInModal( '#appointment_month_to' ).attr( 'min', $( this ).val() );
					// Auto-fill "to" month using combined duration in months (product + add-ons).
					const pd = wp.productData || productData || {};
					var baseMonths = parseInt( pd.duration || 1, 10 );
					var addonMonths = parseInt( wp.findInModal( '#wc_appointments_field_addons_duration' ).val() || '0', 10 );
					var months = baseMonths + ( isNaN( addonMonths ) ? 0 : addonMonths );
					var fromYM = $( this ).val();
					if ( fromYM ) {
						var parts = fromYM.split( '-' );
						var fy = parseInt( parts[ 0 ], 10 );
						var fm = parseInt( parts[ 1 ], 10 ) - 1;
						var end = new Date( fy, fm + Math.max( months - 1, 0 ), 1 );
						var endStr = end.getFullYear() + '-' + ( '0' + ( end.getMonth() + 1 ) ).slice( -2 );
						wp.findInModal( '#appointment_month_to' ).val( endStr );
					}
					// Delegate downstream updates to unified handler to avoid duplicate availability checks
					wp.onRangeChange();
				} );
				wp.findInModal( '#appointment_month_to' ).attr( 'min', ym );
				// If a range was selected in calendar, prefer it and skip defaults
				if ( wp._prefillRange && wp._prefillRange.startTs ) {
					wp.prefillSelectedRange( wp._prefillRange.startTs, wp._prefillRange.endTs );
				} else if ( productData ) {
					const pd = wp.productData || productData || {};
					var baseMonths = parseInt( pd.duration || 1, 10 );
					var addonMonths = parseInt( wp.findInModal( '#wc_appointments_field_addons_duration' ).val() || '0', 10 );
					var months = baseMonths + ( isNaN( addonMonths ) ? 0 : addonMonths );
					var nextMonthDate = new Date( today.getFullYear(), today.getMonth() + Math.max( months - 1, 0 ), 1 );
					var ymNext = nextMonthDate.getFullYear() + '-' + ( '0' + ( nextMonthDate.getMonth() + 1 ) ).slice( -2 );
					wp.findInModal( '#appointment_month_from' ).val( ym );
					wp.findInModal( '#appointment_month_to' ).val( ymNext );
				}
			}
			wp.validateForm();
		},

		initEnhancedSelects: function() {
			var selectMethod = $.fn.selectWoo ? 'selectWoo' : ( $.fn.select2 ? 'select2' : null );
			if ( !selectMethod ) {
				return;
			}

			$( '.wc-appointment-product-search' )[ selectMethod ]( {
				ajax: {
					url: wc_appointment_modal_js_params.ajax_url,
					dataType: 'json',
					delay: 250,
					data: function( params ) {
						return {
							term: params.term,
							action: 'woocommerce_json_search_appointable_products',
							security: wc_appointment_modal_js_params.nonce_search_products
						};
					},
					processResults: function( data ) {
						var results = [];
						if ( data ) {
							$.each( data, function( id, text ) {
								var name = String( text || '' ).replace( /\s*\(#.*$/, '' );
								results.push( {
									id: id,
									text: name
								} );
							} );
						}
						results = $.map( results, function( r ) { r.text = wp.decodeEntities( r.text ); return r; } );
						return {
							results: results
						};
					}
				}
			} );

			$( '.wc-customer-search' )[ selectMethod ]( {
				allowClear: true,
				minimumInputLength: 3,
				ajax: {
					url: wc_appointment_modal_js_params.ajax_url,
					dataType: 'json',
					delay: 250,
					data: function( params ) {
						return {
							term: params.term,
							action: 'woocommerce_json_search_customers',
							security: wc_appointment_modal_js_params.nonce_search_customers
						};
					},
					processResults: function( data ) {
						var results = [];
						if ( data ) {
							$.each( data, function( id, text ) {
								results.push( { id: id, text: text } );
							} );
						}
						results = $.map( results, function( r ) { r.text = wp.decodeEntities( r.text ); return r; } );
						return { results: results };
					}
				}
			} );

			$( '.wc-order-search' )[ selectMethod ]( {
				ajax: {
					url: wc_appointment_modal_js_params.ajax_url,
					dataType: 'json',
					delay: 250,
					data: function( params ) {
						return {
							term: params.term,
							action: 'wc_appointments_json_search_order',
							security: wc_appointment_modal_js_params.nonce_appointment_order
						};
					},
					processResults: function( data ) {
						var results = [];
						if ( data ) {
							$.each( data, function( id, text ) {
								results.push( {
									id: id,
									text: text
								} );
							} );
						}
						results = $.map( results, function( r ) { r.text = wp.decodeEntities( r.text ); return r; } );
						return {
							results: results
						};
					}
				}
			} );

			$( '.wc-enhanced-select' )[ selectMethod ]();
		},

		// Focus the product selection input when the modal opens
		focusProductSelect: function() {
			var $sel = wp.findInModal( '#appointment_product_id' );
			if ( !$sel.length ) { return; }
			var method = $.fn.selectWoo ? 'selectWoo' : ( $.fn.select2 ? 'select2' : null );
			try { $sel.trigger( 'focus' ); } catch ( e ) {}
			if ( method && 'function' === typeof $sel[ method ] ) {
				try { $sel[ method ]( 'open' ); } catch ( _ ) {}
			}
		},

		// ============================================================================
		// Section: Customer & Order Search
		// ----------------------------------------------------------------------------
		// Initializes selectWoo/select2 search widgets for orders and customers.
		// ============================================================================
		initOrderSearch: function() {
			var selectMethod = $.fn.selectWoo ? 'selectWoo' : ( $.fn.select2 ? 'select2' : null );
			if ( !selectMethod ) {
				return;
			}
			$( '.wc-order-search' )[ selectMethod ]( {
				ajax: {
					url: wc_appointment_modal_js_params.ajax_url,
					dataType: 'json',
					delay: 250,
					data: function( params ) {
						return {
							term: params.term,
							action: 'wc_appointments_json_search_order',
							security: wc_appointment_modal_js_params.nonce_appointment_order
						};
					},
					processResults: function( data ) {
						var results = [];
						if ( data ) {
							$.each( data, function( id, text ) {
								results.push( {
									id: id,
									text: text
								} );
							} );
						}
						results = $.map( results, function( r ) { r.text = wp.decodeEntities( r.text ); return r; } );
						return {
							results: results
						};
					}
				}
			} );
		},

		initCustomerSearch: function() {
			var method = $.fn.selectWoo ? 'selectWoo' : ( $.fn.select2 ? 'select2' : null );
			if ( !method ) {
				return;
			}
			$( '#customer_user' )[ method ]( {
				allowClear: true,
				minimumInputLength: 3,
				ajax: {
					url: wc_appointment_modal_js_params.ajax_url,
					dataType: 'json',
					delay: 250,
					data: function( params ) {
						return {
							term: params.term,
							action: 'woocommerce_json_search_customers',
							security: wc_appointment_modal_js_params.nonce_search_customers
						};
					},
					processResults: function( data ) {
						var results = [];
						if ( data ) {
							$.each( data, function( id, text ) {
								results.push( { id: id, text: text } );
							} );
						}
						results = $.map( results, function( r ) { r.text = wp.decodeEntities( r.text ); return r; } );
						return { results: results };
					}
				}
			} );
		},

		onCustomerTypeChange: function() {
			var customerType = $( '#customer_type' ).val();
			var orderType = $( '#order_type' ).val();
			$( '.wc-appointment-existing-customer, .wc-appointment-manual-customer' ).hide();
			if ( 'existing' === customerType ) {
				$( '.wc-appointment-existing-customer' ).show();
				$( '.wc-appointment-billing-details' ).hide();
				if ( !$( '#customer_user' ).hasClass( 'select2-hidden-accessible' ) && !$( '#customer_user' ).hasClass( 'selectWoo-hidden-accessible' ) ) {
					wp.initCustomerSearch();
				}
			} else if ( 'manual' === customerType ) {
				$( '.wc-appointment-manual-customer' ).show();
				$( '.wc-appointment-billing-details' ).hide();
			} else {
				if ( 'existing' !== orderType ) {
					$( '.wc-appointment-billing-details' ).show();
				} else {
					$( '.wc-appointment-billing-details' ).hide();
				}
			}
		},

		validateForm: function() {
			var $form = $( '#wc-appointment-modal-form' );
			var orderType = $( '#order_type' ).val();

			// Validate required fields, excluding add-ons for availability gating
			$form.find( '[required]' ).each( function() {
				var $field = $( this );
				var $container = $field.closest( '.form-field' );
				if ( $container.length && !$container.is( ':visible' ) ) {
					return;
				}
				// Skip billing details when attaching to existing order
				if ( 'existing' === orderType && $field.closest( '.wc-appointment-billing-details' ).length ) {
					return;
				}
				// Skip Product Add-Ons section here; add-ons are validated separately for submission
				if ( $field.closest( '#wc-appointment-addons-section' ).length ) {
					return;
				}
				if ( !$field.val() ) {
					return false; // Break the loop on the first missing required field
				}
			} );

			// Determine if slot inputs are present and valid (controls availability checks)
			var canCheck = false;
			// Value-first detection to avoid stale visibility affecting notice updates
			var d = wp.findInModal( '#appointment_date' ).val();
			var tf = wp.findInModal( '#appointment_time_from' ).val();
			var tt = wp.findInModal( '#appointment_time_to' ).val();
			var dFrom = wp.findInModal( '#appointment_date_from' ).val();
			var dTo = wp.findInModal( '#appointment_date_to' ).val();
			var mFrom = wp.findInModal( '#appointment_month_from' ).val();
			var mTo = wp.findInModal( '#appointment_month_to' ).val();

			if ( d && tf && tt ) {
				var tfm = ( parseInt( tf.substr( 0, 2 ), 10 ) * 60 ) + parseInt( tf.substr( 3, 2 ), 10 );
				var ttm = ( parseInt( tt.substr( 0, 2 ), 10 ) * 60 ) + parseInt( tt.substr( 3, 2 ), 10 );
				if ( ttm >= tfm ) {
					canCheck = true;
				}
			} else if ( dFrom && dTo ) {
				// TIMEZONE NOTE: Date strings (YYYY-MM-DD) are converted to Date objects for comparison.
				// These represent dates in site timezone, and Date comparison works correctly for
				// date-only comparisons regardless of timezone.
				var df = new Date( dFrom.replace( /-/g, '/' ) );
				var dt = new Date( dTo.replace( /-/g, '/' ) );
				if ( dt >= df ) {
					canCheck = true;
				}
			} else if ( mFrom && mTo ) {
				// TIMEZONE NOTE: Month strings (YYYY-MM) are converted to Date objects for comparison.
				// Adding '-01' creates the first day of the month, which is correct for month comparisons.
				var mf = new Date( mFrom + '-01' );
				var mt = new Date( mTo + '-01' );
				if ( mt >= mf ) {
					canCheck = true;
				}
			} else if ( d ) {
				// Daily products without time range
				canCheck = true;
			}
			// For all other cases, canCheck remains false

			// Validate required Product Add-Ons only to control submission enabling
			var addonsValid = true;
			if ( wp._addons_loaded && window.WC_PAO && wp._paoForm && wp._paoForm.contains_required ) {
				if ( wp._paoForm.validation && 'function' === typeof wp._paoForm.validation.validate ) {
					wp._paoForm.validation.validate();
				}
				addonsValid = !!wp._paoForm.isValid();
				// Expand Add-ons section when invalid so validation messages are visible
				if ( !addonsValid ) {
					$( '#wc-appointment-addons-section' ).removeClass( 'collapsed' );
				}
			}

			// Do not disable submit button; invalid fields will be flagged on submit

			// Do not auto-run availability here; only hide/reset when invalid.
			if ( !canCheck ) {
				$( '#appointment-availability-wrap' ).hide();
				wp.lastAvailability = null;
				// Reset last availability key so returning to a valid state triggers a check
				wp._lastAvailabilityKey = null;
			}
		},

		// New helpers for pricing
		calculateCost: function() {
			var productId = $( '#appointment_product_id' ).val();
			if ( !productId ) {
				$( '#appointment_calculated_price_html' ).html( '' );
				$( '#appointment_calculated_price_raw' ).val( '' );
				$( '#appointment_footer_price' ).html( '' );
				return;
			}

			var showSingleDate = $( '.wc-appointment-date-field' ).is( ':visible' );
			var showTimeRange = $( '.wc-appointment-time-range' ).is( ':visible' );
			var showDateRange = $( '.wc-appointment-date-range' ).is( ':visible' );
			var showMonthRange = $( '.wc-appointment-month-range' ).is( ':visible' );

			var posted = {};
			posted[ 'appointable-product-id' ] = productId;
			var qty = parseInt( $( '#appointment_qty' ).val() || '1', 10 );
			posted.quantity = qty;
			var staffId = $( '#appointment_staff_id' ).val();
			if ( staffId ) {
				posted.wc_appointments_field_staff = staffId;
			}

			if ( showSingleDate ) {
				var d = $( '#appointment_date' ).val();
				if ( !d ) {
					return;
				}
				var parts = d.split( '-' );
				posted.wc_appointments_field_start_date_year = parseInt( parts[ 0 ], 10 );
				posted.wc_appointments_field_start_date_month = parseInt( parts[ 1 ], 10 );
				posted.wc_appointments_field_start_date_day = parseInt( parts[ 2 ], 10 );
				if ( showTimeRange ) {
					var tf = $( '#appointment_time_from' ).val();
					if ( tf ) {
						posted.wc_appointments_field_start_date_time = tf;
					}
				}
			} else if ( showDateRange ) {
				var df = $( '#appointment_date_from' ).val();
				if ( !df ) {
					return;
				}
				var p2 = df.split( '-' );
				posted.wc_appointments_field_start_date_year = parseInt( p2[ 0 ], 10 );
				posted.wc_appointments_field_start_date_month = parseInt( p2[ 1 ], 10 );
				posted.wc_appointments_field_start_date_day = parseInt( p2[ 2 ], 10 );
			} else if ( showMonthRange ) {
				var mf = $( '#appointment_month_from' ).val();
				if ( !mf ) {
					return;
				}
				posted.wc_appointments_field_start_date_yearmonth = mf;
			} else {
				return;
			}

			// Include Product Add-Ons cost/duration selections
			var addonsDuration = parseInt( $( '#wc_appointments_field_addons_duration' ).val() || '0', 10 );
			var addonsCost = $( '#wc_appointments_field_addons_cost' ).val() || '0';
			if ( !isNaN( addonsDuration ) ) {
				posted.wc_appointments_field_addons_duration = addonsDuration;
			}
			if ( '' !== addonsCost ) {
				posted.wc_appointments_field_addons_cost = addonsCost;
			}

			// Build a cache key for cost calculation based on form state
			var costKey = [
				'p=', productId,
				'&staff=', ( staffId || '' ),
				'&qty=', qty,
				'&sdY=', posted.wc_appointments_field_start_date_year || '',
				'&sdM=', posted.wc_appointments_field_start_date_month || '',
				'&sdD=', posted.wc_appointments_field_start_date_day || '',
				'&sdHM=', posted.wc_appointments_field_start_date_time || '',
				'&sdYM=', posted.wc_appointments_field_start_date_yearmonth || '',
				'&adDur=', addonsDuration || 0,
				'&adCost=', addonsCost || 0,
				'&tRange=', showTimeRange ? 1 : 0,
				'&dRange=', showDateRange ? 1 : 0,
				'&mRange=', showMonthRange ? 1 : 0
			].join( '' );

			// Use cached cost if available
			var cachedCost = wp._costCache[ costKey ];
			if ( cachedCost ) {
				var innerCached = wp.extractInnerStrong( cachedCost.html );
				var editingCached = $( '#appointment_override_price' ).is( ':visible' );
				if ( innerCached ) {
					$( '#appointment_footer_price' ).html( innerCached );
				}
				if ( undefined !== cachedCost.raw && '' !== cachedCost.raw ) {
					$( '#appointment_calculated_price_raw' ).val( cachedCost.raw );
					if ( !editingCached ) {
						$( '#appointment_override_price_hidden' ).val( cachedCost.raw );
					}
				}
				if ( !editingCached ) {
					$( '#appointment_calculated_price_html' ).html( innerCached );
				}
				wp.syncPAOBasePrice();
				return;
			}

			// Abort previous in-flight cost request if key differs
			if ( wp._costXHR && wp._costInFlightKey && wp._costInFlightKey !== costKey && wp._costXHR.abort ) {
				try {
					wp._costXHR.abort();
				} catch ( e ) {}
			}
			wp._costInFlightKey = costKey;

			wp.showPriceLoading();
			wp._costXHR = $.ajax( {
				url: wc_appointment_modal_js_params.ajax_url,
				type: 'POST',
				data: {
					action: 'wc_appointments_calculate_costs',
					form: $.param( posted )
				}
			} ).done( function( resp ) {
				var html = '';
				var raw = '';
				if ( resp && resp.success ) {
					if ( 'string' === typeof resp.data ) {
						html = resp.data;
					} else if ( resp.data ) {
						html = resp.data.html || '';
						raw = ( resp.data.raw !== undefined ) ? resp.data.raw : '';
					}
				}
				// Cache the computed cost result for this key
				wp._costCache[ costKey ] = {
					html: html,
					raw: raw
				};
				// Show only the inner contents of any <strong> tag from returned HTML
				var inner = wp.extractInnerStrong( html );
				var editing = $( '#appointment_override_price' ).is( ':visible' );
				if ( resp && resp.success ) {
					if ( inner ) {
						$( '#appointment_footer_price' ).html( inner );
					}
					if ( '' !== raw ) {
						$( '#appointment_calculated_price_raw' ).val( raw );
						if ( !editing ) {
							$( '#appointment_override_price_hidden' ).val( raw );
						}
					}
				} else {
					// Fallback: compute base + add-ons locally to avoid blank price
					var baseRawNum = ( wp.productData && wp.productData.price_raw !== undefined ) ? parseFloat( wp.productData.price_raw ) : NaN;
					var addonsNum = parseFloat( $( '#wc_appointments_field_addons_cost' ).val() || '0' );
					var totalNum = ( isNaN( baseRawNum ) ? 0 : baseRawNum ) + ( isNaN( addonsNum ) ? 0 : addonsNum );
					if ( !isNaN( totalNum ) && 0 <= totalNum ) {
						$( '#appointment_calculated_price_raw' ).val( totalNum );
						if ( !editing ) {
							$( '#appointment_override_price_hidden' ).val( totalNum );
						}
						var fallbackHtml = wp.formatPriceHTML( totalNum );
						if ( fallbackHtml ) {
							$( '#appointment_footer_price' ).html( fallbackHtml );
						}
					} else {
						// Keep existing displayed price; ensure base raw/html present
						if ( !$( '#appointment_calculated_price_raw' ).val() && wp.productData && wp.productData.price_raw !== undefined ) {
							$( '#appointment_calculated_price_raw' ).val( wp.productData.price_raw );
							if ( !editing ) {
								$( '#appointment_override_price_hidden' ).val( wp.productData.price_raw );
							}
						}
						if ( !$( '#appointment_footer_price' ).html() && wp.productData && wp.productData.price_html ) {
							$( '#appointment_footer_price' ).html( wp.productData.price_html );
						}
					}
				}
				$( '#appointment_price_edit' ).toggle( !editing );
			} ).fail( function() {
				var baseRaw = wp.productData && wp.productData.price_raw;
				var baseHtml = wp.productData && wp.productData.price_html;
				var addonsNum = parseFloat( $( '#wc_appointments_field_addons_cost' ).val() || '0' );
				var editing = $( '#appointment_override_price' ).is( ':visible' );
				if ( baseRaw !== undefined ) {
					var baseNum = parseFloat( baseRaw );
					var total = ( isNaN( baseNum ) ? 0 : baseNum ) + ( isNaN( addonsNum ) ? 0 : addonsNum );
					if ( !isNaN( total ) ) {
						$( '#appointment_calculated_price_raw' ).val( total );
						if ( !editing ) {
							$( '#appointment_override_price_hidden' ).val( total );
						}
						var html = wp.formatPriceHTML( total ) || baseHtml;
						if ( html ) {
							$( '#appointment_footer_price' ).html( html );
						}
					} else {
						$( '#appointment_calculated_price_raw' ).val( baseRaw );
						if ( !editing ) {
							$( '#appointment_override_price_hidden' ).val( baseRaw );
						}
						if ( !$( '#appointment_footer_price' ).html() && baseHtml ) {
							$( '#appointment_footer_price' ).html( baseHtml );
						}
					}
				} else {
					if ( !$( '#appointment_footer_price' ).html() && baseHtml ) {
						$( '#appointment_footer_price' ).html( baseHtml );
					}
				}
				$( '#appointment_price_edit' ).toggle( !editing );
			} ).always( function() {
				wp.hidePriceLoading();
				wp.syncPAOBasePrice();
			} );
		},
		scheduleCalculateCost: function() {
			if ( wp._costTimer ) {
				clearTimeout( wp._costTimer );
			}
			// Faster feedback while still avoiding excessive requests
			wp._costTimer = setTimeout( wp.calculateCost, 200 );
		},
		// ============================================================================
		// Section: Staff Selection
		// ----------------------------------------------------------------------------
		// Reacts to staff changes: re-validates input, schedules cost recalculation,
		// and triggers availability checks when parameters change.
		// ============================================================================
		onStaffChange: function() {
			wp.validateForm();
			wp.scheduleCalculateCost();
			// Staff affects availability; re-check only if params changed
			wp.maybeCheckAvailability();
		},
		onRangeChange: function() {
			wp.validateForm();
			wp.scheduleCalculateCost();
			// Date/month range affects availability; re-check if params changed
			wp.maybeCheckAvailability();
		},
		onQtyChange: function() {
			var qty = parseInt( $( '#appointment_qty' ).val() || '1', 10 );
			// Sync hidden PAO qty used by Product Add-Ons pricing
			var $addonsSection = $( '#wc-appointment-addons-section' );
			var $qtyHidden = $addonsSection.find( 'input.qty' );
			if ( $qtyHidden.length ) {
				$qtyHidden.val( qty );
				$addonsSection.trigger( 'woocommerce-product-addons-update' );
			}
			wp.validateForm();
			wp.scheduleCalculateCost();
			// Quantity affects availability; check only if params changed
			wp.maybeCheckAvailability();
		},

		// ============================================================================
		// Section: Validation & Submission
		// ----------------------------------------------------------------------------
		// Validates inputs, assembles payload, and submits; errors are reported inline.
		// ============================================================================
		submitForm: function( e ) {
			e.preventDefault();

			// If background tasks are still running, inform user and allow proceed.
			// This prevents surprise if availability/price/add-ons are not yet finalized.
			var bs = wp.getBusyStatus();
			if ( bs && bs.busy ) {
				var baseMsg = ( wc_appointment_modal_js_params.i18n && wc_appointment_modal_js_params.i18n.loading_in_progress ) || 'Background tasks are still running';
				var details = bs.labels && bs.labels.length ? ( ' (' + bs.labels.join( ', ' ) + ')' ) : '';
				var confirmMsg = baseMsg + details + ' — ' + ( ( wc_appointment_modal_js_params.i18n && wc_appointment_modal_js_params.i18n.confirmation ) || ( wc_appointment_modal_js_params.i18n_confirmation || 'Proceed anyway?' ) );
				if ( !window.confirm( confirmMsg ) ) {
					return; // User cancelled; do not proceed
				}
			}

			// Provide immediate feedback
			wp.showLoading();

			// Base required field validation before attempting submission
			var orderType = $( '#order_type' ).val();
			var missing = [];
			$( '#wc-appointment-modal-form' ).find( '[required]' ).each( function() {
				var $field = $( this );
				var $container = $field.closest( '.form-field' );
				if ( $container.length && !$container.is( ':visible' ) ) {
					return;
				}
				// Skip billing details when attaching to existing order
				if ( 'existing' === orderType && $field.closest( '.wc-appointment-billing-details' ).length ) {
					return;
				}
				// Skip Product Add-Ons section here; add-ons validated separately below
				if ( $field.closest( '#wc-appointment-addons-section' ).length ) {
					return;
				}
				if ( !$field.val() ) {
					var label = $container.find( 'label' ).first().text().replace( /\*+$/, '' ).trim() || 'A required field';
					missing.push( label );
				}
			} );
			if ( missing.length ) {
				wp.renderInlineNotice( 'error', 'Please complete required fields: ' + missing.join( ', ' ) );
				wp.hideLoading();
				return;
			}

			// Prevent submission if required add-ons are invalid
			if ( wp._addons_loaded && window.WC_PAO && wp._paoForm && wp._paoForm.contains_required ) {
				if ( wp._paoForm.validation && 'function' === typeof wp._paoForm.validation.validate ) {
					wp._paoForm.validation.validate();
				}
				if ( !wp._paoForm.isValid() ) {
					// Make add-ons visible and bring header into view for clarity
					$( '#wc-appointment-addons-section' ).removeClass( 'collapsed' );
					try {
						document.getElementById( 'appointment_addons_link' ).scrollIntoView( { behavior: 'smooth', block: 'start' } );
					} catch ( ex ) {}
					// Inline notice instead of alert
					wp.renderInlineNotice( 'error', wc_appointment_modal_js_params.i18n_addons_required || 'Please select required add-ons.' );
					wp.hideLoading();
					return;
				}
			}

			if ( false === wp.lastAvailability ) {
				var warning = wc_appointment_modal_js_params.i18n_unavailable + ' — ' + ( wc_appointment_modal_js_params.i18n_confirmation || 'Are you sure?' );
				if ( !window.confirm( warning ) ) {
					// User cancelled submission; hide the spinner we showed earlier
					wp.hideLoading();
					return;
				}
			}

			var $button = $( this );
			var $form = $( '#wc-appointment-modal-form' );
			// Build FormData to include file uploads from Product Add-Ons
			// Using FormData ensures files in inputs like `addon-<field_name>` are transmitted
			var fd;
			if ( window.FormData ) {
				fd = new window.FormData( $form[ 0 ] );
				fd.append( 'action', 'wc_appointments_create_appointment' );
				fd.append( 'nonce', wc_appointment_modal_js_params.nonce_create_appointment );
			} else {
				// Fallback for very old browsers: no file support, but keep behavior
				fd = $form.serialize() + '&action=wc_appointments_create_appointment&nonce=' + wc_appointment_modal_js_params.nonce_create_appointment;
			}
			$button.prop( 'disabled', true ).text( wc_appointment_modal_js_params.i18n.creating );

			// Disable modal content interactions while creating
			var $main = $( '.wc-backbone-modal-main' );
			$main.addClass( 'is-busy' );
			$main.find( '.wc-appointment-modal-content' ).attr( 'inert', '' );

			$.ajax( {
				url: wc_appointment_modal_js_params.ajax_url,
				type: 'POST',
				// When FormData is used, disable default processing and content type
				processData: !( fd instanceof window.FormData ),
				contentType: ( fd instanceof window.FormData ) ? false : 'application/x-www-form-urlencoded; charset=UTF-8',
				data: fd,
				dataType: 'json'
			} ).done( function( response ) {
				if ( response.success ) {
					// Keep the modal open and show a success panel with actions
					try {
						wp.showCreationSuccess( response.data );
					} catch ( ex ) {
						// Fallback to alert if UI injection fails
						alert( ( response.data && response.data.message ) || wc_appointment_modal_js_params.i18n.create_appointment );
					}
				} else {
					wp.renderInlineNotice( 'error', response.data || wc_appointment_modal_js_params.i18n.error_creating );
					$button.prop( 'disabled', false ).text( wc_appointment_modal_js_params.i18n.create_appointment );
				}
			} ).fail( function() {
				wp.renderInlineNotice( 'error', wc_appointment_modal_js_params.i18n.error_creating );
				$button.prop( 'disabled', false ).text( wc_appointment_modal_js_params.i18n.create_appointment );
			} ).always( function() {
				// Re-enable modal content interactions
				var $main = $( '.wc-backbone-modal-main' );
				$main.removeClass( 'is-busy' );
				$main.find( '.wc-appointment-modal-content' ).removeAttr( 'inert' );
				wp.hideLoading();
			} );
		},

		// Decode basic HTML entities in select2/selectWoo results
		decodeEntities: function( text ) {
			try {
				if ( null === text || 'string' !== typeof text ) {
					return text;
				}
				var div = document.createElement( 'div' );
				div.innerHTML = text;
				return div.textContent || div.innerText || text;
			} catch ( e ) {
				return text;
			}
		},

		// Render an inline notice within the modal content area
		renderInlineNotice: function( type, message ) {
			// Insert or replace a single inline notice
			var $main = $( '.wc-backbone-modal-main' );
			var $content = $main.find( '.wc-appointment-modal-content' );
			var cls = 'notice notice-' + ( type || 'info' );
			var $existing = $content.find( '.wc-appointment-inline-notice' );
			if ( $existing.length ) {
				$existing.remove();
			}
			var $notice = $( '<div class="wc-appointment-inline-notice ' + cls + '" role="alert"><p></p></div>' );
			$notice.find( 'p' ).text( message || '' );
			$content.prepend( $notice );

			// Smoothly scroll the modal content to the notice for visibility
			try {
				var top = $notice.position().top;
				$content.stop().animate( { scrollTop: Math.max( top - 16, 0 ) }, 200 );
			} catch ( e ) {}

			// Auto-dismiss on next valid input change: bind one-time handler
			var clearHandler = function() {
				// Re-run lightweight validation; if no missing base fields and add-ons valid, clear notice
				if ( wp && 'function' === typeof wp._isFormValid ) {
					if ( wp._isFormValid() ) {
						wp.clearInlineNotice();
						$( document ).off( 'change keyup', '.wc-backbone-modal .wc-appointment-modal-form input, .wc-backbone-modal .wc-appointment-modal-form select', clearHandler );
					}
				} else {
					// Fallback: clear on first change
					wp.clearInlineNotice();
					$( document ).off( 'change keyup', '.wc-backbone-modal .wc-appointment-modal-form input, .wc-backbone-modal .wc-appointment-modal-form select', clearHandler );
				}
			};
			$( document ).on( 'change keyup', '.wc-backbone-modal .wc-appointment-modal-form input, .wc-backbone-modal .wc-appointment-modal-form select', clearHandler );
		},

		// Remove any inline notice
		clearInlineNotice: function() {
			var $main = $( '.wc-backbone-modal-main' );
			$main.find( '.wc-appointment-inline-notice' ).remove();
		},

		// Lightweight validity check used for auto-dismiss
		_isFormValid: function() {
			try {
				var orderType = $( '#order_type' ).val();
				var missing = [];
				$( '#wc-appointment-modal-form' ).find( '[required]' ).each( function() {
					var $field = $( this );
					var $container = $field.closest( '.form-field' );
					if ( $container.length && !$container.is( ':visible' ) ) {
						return;
					}
					if ( 'existing' === orderType && $field.closest( '.wc-appointment-billing-details' ).length ) {
						return;
					}
					if ( $field.closest( '#wc-appointment-addons-section' ).length ) {
						return;
					}
					if ( !$field.val() ) {
						missing.push( 'x' );
					}
				} );
				// Validate required add-ons if loaded
				if ( wp._addons_loaded && window.WC_PAO && wp._paoForm && wp._paoForm.contains_required ) {
					if ( wp._paoForm.validation && 'function' === typeof wp._paoForm.validation.validate ) {
						wp._paoForm.validation.validate();
					}
					if ( !wp._paoForm.isValid() ) {
						missing.push( 'addons' );
					}
				}
				return 0 === missing.length;
			} catch ( e ) {
				return false;
			}
		},

		// Show post-submit success state and provide actions without reloading
		showCreationSuccess: function( data ) {
			var msg = ( data && data.message ) || wc_appointment_modal_js_params.i18n.appointment_created || 'Appointment created.';
			var appointmentId = data && data.appointment_id ? parseInt( data.appointment_id, 10 ) : 0;
			var orderId = data && data.order_id ? parseInt( data.order_id, 10 ) : 0;
			var adminBase = ( wc_appointment_modal_js_params.ajax_url || '' ).split( 'admin-ajax.php' )[0];
			var appointmentUrl = appointmentId ? ( adminBase + 'post.php?post=' + appointmentId + '&action=edit' ) : '';
			var orderUrl = orderId ? ( adminBase + 'post.php?post=' + orderId + '&action=edit' ) : '';
			var i18n = wc_appointment_modal_js_params.i18n || {};
			var labelEditAppointment = i18n.edit_appointment || 'Edit Appointment';
			var labelEditOrder = i18n.edit_order || 'Edit Order';
			var ctxParts = [];
			var pd = wp.productData || {};
			var pText = '';
			if ( pd && pd.formatted_name ) { pText = pd.formatted_name; }
			if ( !pText ) {
				var $sel = $( '#appointment_product_id' );
				var $opt = $sel.find( 'option:selected' );
				if ( $opt.length ) { pText = $opt.text().trim(); }
			}
			if ( pText ) { ctxParts.push( pText ); }
			var d = $( '#appointment_date' ).val();
			var tf = $( '#appointment_time_from' ).val();
			var tt = $( '#appointment_time_to' ).val();
			var df = $( '#appointment_date_from' ).val();
			var dt = $( '#appointment_date_to' ).val();
			var mf = $( '#appointment_month_from' ).val();
			var mt = $( '#appointment_month_to' ).val();
			if ( d ) {
				var formattedDate = wp.formatDateForDisplay( d );
				var formattedTimeFrom = tf ? wp.formatHHMMForDisplay( tf ) : '';
				var formattedTimeTo = tt ? wp.formatHHMMForDisplay( tt ) : '';
				ctxParts.push( formattedTimeFrom && formattedTimeTo ? ( formattedDate + ' ' + formattedTimeFrom + '–' + formattedTimeTo ) : formattedDate );
			} else if ( df ) {
				var formattedDateFrom = wp.formatDateForDisplay( df );
				var formattedDateTo = dt && dt !== df ? wp.formatDateForDisplay( dt ) : '';
				ctxParts.push( formattedDateTo ? ( formattedDateFrom + ' → ' + formattedDateTo ) : formattedDateFrom );
			} else if ( mf ) {
				var formattedMonthFrom = wp.formatMonthForDisplay( mf );
				var formattedMonthTo = mt && mt !== mf ? wp.formatMonthForDisplay( mt ) : '';
				ctxParts.push( formattedMonthTo ? ( formattedMonthFrom + ' → ' + formattedMonthTo ) : formattedMonthFrom );
			}
			var finalMsg = ( ctxParts.length ? ( ctxParts.join( ' • ' ) + ' — ' ) : '' ) + msg;

			var $main = $( '.wc-backbone-modal-main' );
			var $content = $main.find( '.wc-appointment-modal-content' );
			var $footer = $main.find( '.wc-backbone-modal-footer .inner' );

			// Remove any previous success panel and footer actions to avoid duplicates
			$content.find( '#wc-appointment-success-panel' ).remove();
			$footer.find( '.wc-appointment-success-actions' ).remove();

			// Hide the form
			$main.find( '#wc-appointment-modal-form' ).hide();

			// Show success notice in content
			var html = '';
			html += '<div id="wc-appointment-success-panel" class="wc-appointment-success">';
			html += '  <div class="notice notice-success"><p>' + finalMsg + '</p></div>';
			// Top actions under the notice: view appointment/order and back to list
			var topHtml = '';
			var hasTop = false;
			topHtml += '<div class="wc-appointment-success-actions-top">';
			if ( appointmentUrl ) {
				topHtml += '  <a class="button" id="btn-view-appointment" href="' + appointmentUrl + '" target="_blank">' + labelEditAppointment + '</a>';
				hasTop = true;
			}
			if ( orderUrl ) {
				topHtml += '  <a class="button" id="btn-view-order" href="' + orderUrl + '" target="_blank">' + labelEditOrder + '</a>';
				hasTop = true;
			}
			topHtml += '</div>';
			if ( hasTop ) {
				html += topHtml;
			}
			html += '</div>';
			var $panel = $( html );
			$content.append( $panel );

			// Notify React calendar to refresh immediately
			try {
				if ( window.wcAppointmentsRefreshCalendar && 'function' === typeof window.wcAppointmentsRefreshCalendar ) {
					window.wcAppointmentsRefreshCalendar();
				}
				// Also dispatch a custom event for any listeners
				var ev = new Event( 'wc_appointments_created' );
				window.dispatchEvent( ev );
			} catch ( e ) {}

			// Hide price area in footer and original action buttons
			$footer.find( '.wc-appointment-footer-price' ).hide();
			$footer.find( '#btn-ok' ).hide();
			$footer.find( '#btn-cancel' ).hide();

			// Footer success actions will be appended fresh below
			// Build success actions in footer for consistency
			var actions = '<div class="wc-appointment-success-actions">';
			actions += '  <button type="button" id="btn-close-success" class="button modal-close">' + ( wc_appointment_modal_js_params.i18n.close || 'Close' ) + '</button>';
			actions += '  <button type="button" id="btn-create-another" class="button button-primary">' + ( wc_appointment_modal_js_params.i18n.create_another || 'Create Another' ) + '</button>';
			actions += '</div>';
			$footer.append( actions );

			// Wire "Create Another" in footer
			$footer.on( 'click', '#btn-create-another', function() {
				try {
					wp._rebuildingModal = false;
					wp.rebuildModalForProduct( null, null );
				} catch ( e ) {
					wp.closeModal();
					wp.openModal();
				}
			} );
		}
	} );

	// Ensure modal handlers are bound even if writepanel init ran earlier
	wp.initModal();

	// Intercept default "Add New" links for Appointments and open modal instantly
	$( document ).on( 'click', 'a[href*="post-new.php"][href*="post_type=wc_appointment"]', function( e ) {
		// Allow explicit bypass for links marked to skip modal interception
		if ( $( e.currentTarget ).is( '[data-no-modal], [data-no-modal="1"]' ) ) {
			return; // proceed with default navigation
		}
		e.preventDefault();
		wp.openModal();
	} );

	// Intercept Calendar page header button (legacy add_appointment page) and open modal instead
	$( document ).on( 'click', 'a[href*="edit.php"][href*="post_type=wc_appointment"][href*="page=add_appointment"]', function( e ) {
		// Allow explicit bypass for links marked to skip modal interception
		if ( $( e.currentTarget ).is( '[data-no-modal], [data-no-modal="1"]' ) ) {
			return; // proceed with default navigation
		}
		e.preventDefault();
		wp.openModal();
	} );

	( function() {
		try {
			var params = new URLSearchParams( window.location.search || '' );
			if ( '1' === params.get( 'open_modal' ) ) {
				wp.openModal();
			}
		} catch ( err ) {}
	} )();
} );
