<?php
/**
 * Frontend table reservation form and AJAX handlers.
 *
 * @package FoodOrd
 */

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

/**
 * Class Foodord_Table_Reservations_Frontend
 */
class Foodord_Table_Reservations_Frontend {
	private const NONCE_ACTION            = 'foodord_table_reservations';
	private const OPTION_BANNER_IMAGE_ID  = 'foodord_table_reservation_banner_image_id';
	private const OPTION_BANNER_DISABLED  = 'foodord_table_reservation_banner_disabled';
	private const RECAPTCHA_ACTION_HOLD   = 'foodord_reservation_hold';
	private const RECAPTCHA_ACTION_SUBMIT = 'foodord_reservation_submit';
	private const MANAGE_NONCE_ACTION     = 'foodord_manage_reservation';

	/**
	 * Core reservations service.
	 *
	 * @var Foodord_Table_Reservations
	 */
	private $reservations;

	/**
	 * Reservation customer emails.
	 *
	 * @var Foodord_Table_Reservation_Emails
	 */
	private $emails;

	/**
	 * Reservation pre-order lifecycle service.
	 *
	 * @var Foodord_Table_Reservation_Preorders
	 */
	private $preorders;

	/**
	 * Whether the current request renders the customer management screen.
	 *
	 * @var bool
	 */
	private $is_manage_request = false;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->reservations = new Foodord_Table_Reservations();
		$this->emails       = new Foodord_Table_Reservation_Emails();
		$this->preorders    = new Foodord_Table_Reservation_Preorders();
	}

	/**
	 * Bootstrap frontend hooks.
	 */
	public function init(): void {
		add_action( 'init', array( $this, 'register_shortcode' ) );
		add_action( 'wp_ajax_foodord_reservation_get_time_slots', array( $this, 'ajax_get_time_slots' ) );
		add_action( 'wp_ajax_nopriv_foodord_reservation_get_time_slots', array( $this, 'ajax_get_time_slots' ) );
		add_action( 'wp_ajax_foodord_reservation_get_selectable_tables', array( $this, 'ajax_get_selectable_tables' ) );
		add_action( 'wp_ajax_nopriv_foodord_reservation_get_selectable_tables', array( $this, 'ajax_get_selectable_tables' ) );
		add_action( 'wp_ajax_foodord_reservation_manage_time_slots', array( $this, 'ajax_get_manage_time_slots' ) );
		add_action( 'wp_ajax_nopriv_foodord_reservation_manage_time_slots', array( $this, 'ajax_get_manage_time_slots' ) );
		add_action( 'wp_ajax_foodord_reservation_create_hold', array( $this, 'ajax_create_hold' ) );
		add_action( 'wp_ajax_nopriv_foodord_reservation_create_hold', array( $this, 'ajax_create_hold' ) );
		add_action( 'wp_ajax_foodord_reservation_submit', array( $this, 'ajax_submit_reservation' ) );
		add_action( 'wp_ajax_nopriv_foodord_reservation_submit', array( $this, 'ajax_submit_reservation' ) );
		add_action( 'wp_ajax_foodord_reservation_release_hold', array( $this, 'ajax_release_hold' ) );
		add_action( 'wp_ajax_nopriv_foodord_reservation_release_hold', array( $this, 'ajax_release_hold' ) );
		add_action( 'wp_ajax_foodord_reservation_preorder_start', array( $this, 'ajax_start_preorder' ) );
		add_action( 'wp_ajax_nopriv_foodord_reservation_preorder_start', array( $this, 'ajax_start_preorder' ) );
		add_action( 'wp_ajax_foodord_reservation_preorder_resume', array( $this, 'ajax_resume_preorder' ) );
		add_action( 'wp_ajax_nopriv_foodord_reservation_preorder_resume', array( $this, 'ajax_resume_preorder' ) );
		add_action( 'wp_ajax_foodord_reservation_preorder_cancel', array( $this, 'ajax_cancel_preorder' ) );
		add_action( 'wp_ajax_nopriv_foodord_reservation_preorder_cancel', array( $this, 'ajax_cancel_preorder' ) );
		add_action( 'wp_ajax_foodord_reservation_preorder_leave', array( $this, 'ajax_leave_preorder' ) );
		add_action( 'wp_ajax_nopriv_foodord_reservation_preorder_leave', array( $this, 'ajax_leave_preorder' ) );
		add_action( 'wp_ajax_foodord_reservation_preorder_add_item', array( $this, 'ajax_add_preorder_item' ) );
		add_action( 'wp_ajax_nopriv_foodord_reservation_preorder_add_item', array( $this, 'ajax_add_preorder_item' ) );
		add_action( 'wp_ajax_foodord_reservation_preorder_update_item', array( $this, 'ajax_update_preorder_item' ) );
		add_action( 'wp_ajax_nopriv_foodord_reservation_preorder_update_item', array( $this, 'ajax_update_preorder_item' ) );
		add_action( 'wp_ajax_foodord_reservation_preorder_clear_items', array( $this, 'ajax_clear_preorder_items' ) );
		add_action( 'wp_ajax_nopriv_foodord_reservation_preorder_clear_items', array( $this, 'ajax_clear_preorder_items' ) );
		add_action( 'wp_ajax_foodord_reservation_preorder_summary', array( $this, 'ajax_get_preorder_summary' ) );
		add_action( 'wp_ajax_nopriv_foodord_reservation_preorder_summary', array( $this, 'ajax_get_preorder_summary' ) );
		add_action( 'wp_ajax_foodord_reservation_preorder_checkout', array( $this, 'ajax_checkout_preorder' ) );
		add_action( 'wp_ajax_nopriv_foodord_reservation_preorder_checkout', array( $this, 'ajax_checkout_preorder' ) );
		add_action( 'admin_post_foodord_manage_reservation_update', array( $this, 'handle_manage_reservation_update' ) );
		add_action( 'admin_post_nopriv_foodord_manage_reservation_update', array( $this, 'handle_manage_reservation_update' ) );
		add_action( 'admin_post_foodord_manage_reservation_cancel', array( $this, 'handle_manage_reservation_cancel' ) );
		add_action( 'admin_post_nopriv_foodord_manage_reservation_cancel', array( $this, 'handle_manage_reservation_cancel' ) );
		add_action( 'template_redirect', array( $this, 'maybe_render_manage_page' ) );
		add_action( 'wp_enqueue_scripts', array( $this, 'maybe_enqueue_manage_assets' ) );
	}

	/**
	 * Register shortcode.
	 */
	public function register_shortcode(): void {
		add_shortcode( 'foodord_reservations', array( $this, 'render_shortcode' ) );
	}

	/**
	 * Render reservation form.
	 *
	 * @return string
	 */
	public function render_shortcode(): string {
		$this->enqueue_assets();

		if ( $this->is_manage_request() ) {
			$this->is_manage_request = true;

			ob_start();
			$this->render_manage_page();

			return (string) ob_get_clean();
		}

		$has_tables                    = ! empty( $this->reservations->get_tables( false ) );
		$banner_url                    = $this->get_banner_image_url();
		$root_class                    = 'foodord-table-reservations ' . ( '' !== $banner_url ? 'foodord-table-reservations--with-banner' : 'foodord-table-reservations--no-banner' );
		$can_manage_reservations       = current_user_can( 'manage_options' );
		$quota_status                  = class_exists( 'Foodord_Reservation_Quota' ) ? Foodord_Reservation_Quota::get_status() : array();
		$quota_exhausted               = ! empty( $quota_status['known'] ) && empty( $quota_status['allowed'] );
		$table_selection_enabled       = Foodord_Table_Reservations::is_frontend_table_selection_enabled();
		$preorder_settings             = Foodord_Table_Reservation_Preorders::get_settings();
		$preorder_edit_context         = $this->get_preorder_edit_context();
		$preorder_products             = $preorder_settings['enabled'] || ! empty( $preorder_edit_context ) ? $this->preorders->get_catalog_products() : array();
		$privacy_settings              = Foodord_Table_Reservations::get_privacy_settings();
		$deposit_success_payload       = $this->get_deposit_success_payload();
		$cancelled_payment_reservation = class_exists( 'Foodord_Table_Reservation_Deposits' )
			? Foodord_Table_Reservation_Deposits::get_cancelled_return_reservation_from_request( $this->reservations )
			: null;
		$deposit_cancelled             = null !== $cancelled_payment_reservation;
		$cancelled_payment_is_preorder = $cancelled_payment_reservation
			&& Foodord_Table_Reservation_Deposits::reservation_has_preorder( $cancelled_payment_reservation );
		$privacy_page_url              = '';
		$privacy_page_title            = '';
		$privacy_page_id               = absint( $privacy_settings['privacy_page_id'] );
		if ( $privacy_settings['enabled'] && $privacy_page_id > 0 && 'publish' === get_post_status( $privacy_page_id ) ) {
			$page_url = get_permalink( $privacy_page_id );
			if ( is_string( $page_url ) ) {
				$privacy_page_url   = $page_url;
				$privacy_page_title = get_the_title( $privacy_page_id );
			}
		}
		$reservations_admin_url = $can_manage_reservations
			? admin_url( 'admin.php?page=' . $this->get_reservations_admin_page_slug() )
			: '';
		$license_admin_url      = $can_manage_reservations
			? ( function_exists( 'foodord_get_admin_page_url' ) ? foodord_get_admin_page_url( 'license' ) : admin_url( 'admin.php?page=foodord-license' ) )
			: '';
		$form_instance_id       = wp_unique_id( 'foodord-reservation-form-' );
		$form_control_ids       = array(
			'date'                  => $form_instance_id . '-date',
			'time'                  => $form_instance_id . '-time',
			'party_size'            => $form_instance_id . '-party-size',
			'name'                  => $form_instance_id . '-name',
			'name_error'            => $form_instance_id . '-name-error',
			'email'                 => $form_instance_id . '-email',
			'email_error'           => $form_instance_id . '-email-error',
			'phone'                 => $form_instance_id . '-phone',
			'phone_error'           => $form_instance_id . '-phone-error',
			'note'                  => $form_instance_id . '-note',
			'privacy_consent'       => $form_instance_id . '-privacy-consent',
			'privacy_consent_error' => $form_instance_id . '-privacy-consent-error',
			'preorder_cart_title'   => $form_instance_id . '-preorder-cart-title',
			'preorder_clear_title'  => $form_instance_id . '-preorder-clear-title',
			'preorder_leave_title'  => $form_instance_id . '-preorder-leave-title',
		);

		ob_start();
		?>
		<div class="<?php echo esc_attr( $root_class ); ?>" data-foodord-reservations data-foodord-reservation-step="time" data-foodord-preorder-enabled="<?php echo esc_attr( $preorder_settings['enabled'] ? '1' : '0' ); ?>" data-foodord-preorder-mode="<?php echo esc_attr( $preorder_settings['mode'] ); ?>" data-foodord-preorder-flow="0" <?php echo ! empty( $deposit_success_payload ) ? 'data-foodord-reservation-success-payload="' . esc_attr( wp_json_encode( $deposit_success_payload ) ) . '"' : ''; ?> <?php echo ! empty( $preorder_edit_context ) ? 'data-foodord-preorder-edit-payload="' . esc_attr( wp_json_encode( $preorder_edit_context ) ) . '"' : ''; ?>>
			<div class="foodord-table-reservations__layout">
				<?php if ( '' !== $banner_url ) : ?>
					<div class="foodord-table-reservations__banner" aria-hidden="true">
						<img src="<?php echo esc_url( $banner_url ); ?>" alt="" loading="lazy" />
					</div>
				<?php endif; ?>

				<div class="foodord-table-reservations__content">
					<?php if ( $deposit_cancelled ) : ?>
						<div class="foodord-table-reservations__deposit-cancelled" role="status" data-foodord-reservation-deposit-cancelled-notice>
							<span><?php echo esc_html( $cancelled_payment_is_preorder ? __( 'Pre-order payment was cancelled. Your table is no longer being held.', 'foodord' ) : __( 'Deposit payment was cancelled. Your table is no longer being held.', 'foodord' ) ); ?></span>
							<button type="button" class="foodord-table-reservations__deposit-cancelled-close" aria-label="<?php esc_attr_e( 'Close', 'foodord' ); ?>" data-foodord-reservation-deposit-cancelled-close>
								<span aria-hidden="true">&times;</span>
							</button>
							<span class="foodord-table-reservations__deposit-cancelled-progress" aria-hidden="true"></span>
						</div>
					<?php endif; ?>
					<?php if ( $quota_exhausted ) : ?>
						<?php $this->render_reservation_quota_empty_state( $can_manage_reservations, $license_admin_url ); ?>
					<?php elseif ( ! $has_tables ) : ?>
						<?php $this->render_reservations_empty_state( $can_manage_reservations, $reservations_admin_url ); ?>
					<?php else : ?>
						<span class="foodord-table-reservations__form-icon" aria-hidden="true">
							<svg viewBox="0 0 24 24" focusable="false">
								<path d="M8 2v4"></path>
								<path d="M16 2v4"></path>
								<rect x="3" y="4" width="18" height="18" rx="2"></rect>
								<path d="M3 10h18"></path>
								<path d="m9 16 2 2 4-4"></path>
							</svg>
						</span>
						<h2
							class="foodord-table-reservations__form-title"
							data-foodord-reservation-form-title
							data-default-text="<?php echo esc_attr__( 'Make a Reservation', 'foodord' ); ?>"
							data-table-selection-text="<?php echo esc_attr__( 'Choose a table', 'foodord' ); ?>"
						><?php esc_html_e( 'Make a Reservation', 'foodord' ); ?></h2>
						<p
							class="foodord-table-reservations__form-description"
							data-foodord-reservation-form-description
							data-default-text="<?php echo esc_attr__( 'Choose a date, time, and party size to find an available table.', 'foodord' ); ?>"
							data-table-selection-text="<?php echo esc_attr__( 'Select an available table to continue.', 'foodord' ); ?>"
						><?php esc_html_e( 'Choose a date, time, and party size to find an available table.', 'foodord' ); ?></p>
						<?php $this->render_reservation_timeline(); ?>
						<form class="foodord-table-reservations__form" data-foodord-reservation-form novalidate>
							<div class="foodord-table-reservations__section" data-foodord-reservation-slot>
								<div class="foodord-table-reservations__grid">
									<label class="foodord-table-reservations__field foodord-table-reservations__field--date foodord-table-reservations__field--required" for="<?php echo esc_attr( $form_control_ids['date'] ); ?>">
										<span><?php esc_html_e( 'Date', 'foodord' ); ?></span>
										<input id="<?php echo esc_attr( $form_control_ids['date'] ); ?>" class="foodord-table-reservations__control" type="text" inputmode="numeric" autocomplete="off" name="date" placeholder="<?php echo esc_attr__( 'Date', 'foodord' ); ?>" required />
									</label>

									<label class="foodord-table-reservations__field foodord-table-reservations__field--time foodord-table-reservations__field--required" for="<?php echo esc_attr( $form_control_ids['time'] ); ?>">
										<span><?php esc_html_e( 'Time', 'foodord' ); ?></span>
										<select id="<?php echo esc_attr( $form_control_ids['time'] ); ?>" class="foodord-table-reservations__control" name="time" required disabled>
											<option value=""><?php esc_html_e( 'Time', 'foodord' ); ?></option>
										</select>
									</label>

									<label class="foodord-table-reservations__field foodord-table-reservations__field--required" for="<?php echo esc_attr( $form_control_ids['party_size'] ); ?>">
										<span><?php esc_html_e( 'Guests', 'foodord' ); ?></span>
									<input id="<?php echo esc_attr( $form_control_ids['party_size'] ); ?>" class="foodord-table-reservations__control" type="number" name="party_size" min="1" step="1" value="" placeholder="<?php echo esc_attr__( 'Guests', 'foodord' ); ?>" data-pending-value="1" required disabled />
									</label>
								</div>

								<div class="foodord-table-reservations__message" data-foodord-reservation-message role="status" aria-live="polite"></div>

								<button class="foodord-table-reservations__button foodord-table-reservations__button--secondary foodord-table-reservations__button--availability" type="button" data-foodord-reservation-hold>
									<?php esc_html_e( 'Next step', 'foodord' ); ?>
									<svg class="foodord-table-reservations__button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
										<path d="M5 12h14"></path>
										<path d="m13 6 6 6-6 6"></path>
									</svg>
								</button>
							</div>

							<?php if ( $table_selection_enabled ) : ?>
								<div class="foodord-table-reservations__section foodord-table-reservations__table-selection" data-foodord-reservation-table-selection hidden>
									<button class="foodord-table-reservations__table-selection-back" type="button" data-foodord-reservation-table-selection-back>
										<svg class="foodord-table-reservations__table-selection-back-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
											<path d="M19 12H5"></path>
											<path d="m11 6-6 6 6 6"></path>
										</svg>
										<span><?php esc_html_e( 'Back', 'foodord' ); ?></span>
									</button>
									<div class="foodord-table-reservations__table-selection-floors" data-foodord-reservation-table-selection-floors></div>
									<div class="foodord-table-reservations__table-selection-plan" data-foodord-reservation-table-selection-plan></div>
									<div class="foodord-table-reservations__message" data-foodord-reservation-table-selection-message role="status" aria-live="polite"></div>
								</div>
							<?php endif; ?>

							<div class="foodord-table-reservations__hold" data-foodord-reservation-hold-panel hidden>
								<span>
									<span
										data-foodord-reservation-hold-text
										data-default-text="<?php echo esc_attr__( 'We found a table for you and are holding it for', 'foodord' ); ?>"
										data-selected-text="<?php echo esc_attr__( '{table} is being held for you for', 'foodord' ); ?>"
										data-edit-text="<?php echo esc_attr__( 'Your reservation is held while you update your pre-order for', 'foodord' ); ?>"
									><?php esc_html_e( 'We found a table for you and are holding it for', 'foodord' ); ?></span>
									<strong data-foodord-reservation-countdown>05:00</strong>
								</span>
								<button class="foodord-table-reservations__hold-cancel" type="button" data-foodord-reservation-preorder-leave aria-label="<?php echo esc_attr__( 'Cancel reservation', 'foodord' ); ?>">
									<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
										<path d="M8 2v4"></path>
										<path d="M16 2v4"></path>
										<path d="M3 10h18"></path>
										<path d="M19 13V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2h8"></path>
										<path d="m17 17 4 4"></path>
										<path d="m21 17-4 4"></path>
									</svg>
								</button>
							</div>

							<div class="foodord-table-reservations__section" data-foodord-reservation-customer hidden>
								<div class="foodord-table-reservations__grid">
									<label class="foodord-table-reservations__field foodord-table-reservations__field--required" for="<?php echo esc_attr( $form_control_ids['name'] ); ?>">
										<span><?php esc_html_e( 'Name', 'foodord' ); ?></span>
										<input id="<?php echo esc_attr( $form_control_ids['name'] ); ?>" class="foodord-table-reservations__control" type="text" name="name" autocomplete="name" placeholder="<?php echo esc_attr__( 'Name', 'foodord' ); ?>" aria-describedby="<?php echo esc_attr( $form_control_ids['name_error'] ); ?>" required />
										<small id="<?php echo esc_attr( $form_control_ids['name_error'] ); ?>" class="foodord-table-reservations__field-error" data-foodord-reservation-field-error="name" aria-live="polite"></small>
									</label>

									<label class="foodord-table-reservations__field foodord-table-reservations__field--required" for="<?php echo esc_attr( $form_control_ids['email'] ); ?>">
										<span><?php esc_html_e( 'Email', 'foodord' ); ?></span>
										<input id="<?php echo esc_attr( $form_control_ids['email'] ); ?>" class="foodord-table-reservations__control" type="email" name="email" autocomplete="email" placeholder="<?php echo esc_attr__( 'Email', 'foodord' ); ?>" aria-describedby="<?php echo esc_attr( $form_control_ids['email_error'] ); ?>" required />
										<small id="<?php echo esc_attr( $form_control_ids['email_error'] ); ?>" class="foodord-table-reservations__field-error" data-foodord-reservation-field-error="email" aria-live="polite"></small>
									</label>

									<label class="foodord-table-reservations__field foodord-table-reservations__field--required" for="<?php echo esc_attr( $form_control_ids['phone'] ); ?>">
										<span><?php esc_html_e( 'Phone', 'foodord' ); ?></span>
										<input id="<?php echo esc_attr( $form_control_ids['phone'] ); ?>" class="foodord-table-reservations__control" type="tel" name="phone" autocomplete="tel" placeholder="<?php echo esc_attr__( 'Phone', 'foodord' ); ?>" aria-describedby="<?php echo esc_attr( $form_control_ids['phone_error'] ); ?>" required />
										<small id="<?php echo esc_attr( $form_control_ids['phone_error'] ); ?>" class="foodord-table-reservations__field-error" data-foodord-reservation-field-error="phone" aria-live="polite"></small>
									</label>
								</div>

								<label class="foodord-table-reservations__field foodord-table-reservations__field--note" for="<?php echo esc_attr( $form_control_ids['note'] ); ?>">
									<span><?php esc_html_e( 'Additional Request', 'foodord' ); ?></span>
									<textarea id="<?php echo esc_attr( $form_control_ids['note'] ); ?>" class="foodord-table-reservations__control" name="note" rows="3"></textarea>
								</label>

								<?php if ( $privacy_settings['enabled'] ) : ?>
									<div class="foodord-table-reservations__privacy">
										<div class="foodord-table-reservations__privacy-consent">
											<input
												id="<?php echo esc_attr( $form_control_ids['privacy_consent'] ); ?>"
												type="checkbox"
												name="privacy_consent"
												value="1"
												aria-describedby="<?php echo esc_attr( $form_control_ids['privacy_consent_error'] ); ?>"
												required
											/>
											<p class="foodord-table-reservations__privacy-text">
												<label class="foodord-table-reservations__privacy-label" for="<?php echo esc_attr( $form_control_ids['privacy_consent'] ); ?>"><?php echo esc_html( $privacy_settings['statement'] ); ?></label>
												<?php if ( '' !== $privacy_page_url ) : ?>
													<a class="foodord-table-reservations__privacy-link" href="<?php echo esc_url( $privacy_page_url ); ?>" target="_blank" rel="noopener noreferrer">
														<?php echo esc_html( '' !== $privacy_page_title ? $privacy_page_title : __( 'Privacy Policy', 'foodord' ) ); ?>
													</a>
												<?php endif; ?>
											</p>
										</div>
										<small id="<?php echo esc_attr( $form_control_ids['privacy_consent_error'] ); ?>" class="foodord-table-reservations__field-error foodord-table-reservations__privacy-error" data-foodord-reservation-field-error="privacy_consent" aria-live="polite"></small>
									</div>
								<?php endif; ?>

								<div class="foodord-table-reservations__message" data-foodord-reservation-message role="status" aria-live="polite"></div>

								<div class="foodord-table-reservations__customer-actions">
									<button class="foodord-table-reservations__button foodord-table-reservations__button--cancel" type="button" data-foodord-reservation-cancel-hold>
										<?php esc_html_e( 'Cancel', 'foodord' ); ?>
									</button>
									<button class="foodord-table-reservations__button foodord-table-reservations__button--primary" type="submit" data-foodord-reservation-submit>
										<span data-foodord-reservation-submit-label data-default-text="<?php echo esc_attr__( 'Make reservation', 'foodord' ); ?>" data-preorder-text="<?php echo esc_attr__( 'Continue', 'foodord' ); ?>" data-preorder-required-text="<?php echo esc_attr__( 'Pre-order', 'foodord' ); ?>"><?php esc_html_e( 'Make reservation', 'foodord' ); ?></span>
									</button>
								</div>
							</div>

							<div class="foodord-table-reservations__success" data-foodord-reservation-success role="status" tabindex="-1" hidden>
								<span class="foodord-table-reservations__success-icon" aria-hidden="true"></span>
								<h3 class="foodord-table-reservations__success-title" data-foodord-reservation-success-title><?php esc_html_e( 'Reservation received', 'foodord' ); ?></h3>
								<p class="foodord-table-reservations__success-text" data-foodord-reservation-success-text><?php esc_html_e( 'We have sent the reservation details to your email address.', 'foodord' ); ?></p>
								<p class="foodord-table-reservations__success-code-row" data-foodord-reservation-success-code-row hidden>
									<span><?php esc_html_e( 'Reservation code', 'foodord' ); ?></span>
									<strong data-foodord-reservation-success-code></strong>
								</p>
								<button class="foodord-table-reservations__button foodord-table-reservations__button--home" type="button" data-foodord-reservation-home>
									<svg class="foodord-table-reservations__button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
										<path d="M3 10.5 12 3l9 7.5"></path>
										<path d="M5 10v10h14V10"></path>
										<path d="M9 20v-6h6v6"></path>
									</svg>
									<?php esc_html_e( 'Back to home', 'foodord' ); ?>
								</button>
							</div>

							<?php if ( $preorder_settings['enabled'] ) : ?>
								<div class="foodord-table-reservations__section foodord-table-reservations__preorder-choice" data-foodord-reservation-preorder-choice tabindex="-1" hidden>
									<div class="foodord-table-reservations__preorder-choice-content">
										<h3 class="foodord-table-reservations__preorder-choice-title"><?php esc_html_e( 'Would you like your meal to be ready when you arrive?', 'foodord' ); ?></h3>
										<p class="foodord-table-reservations__preorder-choice-text"><?php esc_html_e( 'Pre-order now so your meal can be prepared for your arrival.', 'foodord' ); ?></p>
									</div>
									<div class="foodord-table-reservations__message" data-foodord-reservation-message role="status" aria-live="polite"></div>
									<div class="foodord-table-reservations__preorder-choice-actions">
										<button class="foodord-table-reservations__button foodord-table-reservations__button--primary" type="button" data-foodord-reservation-preorder-accept><?php esc_html_e( 'Yes, pre-order now', 'foodord' ); ?></button>
										<button class="foodord-table-reservations__button foodord-table-reservations__button--secondary" type="button" data-foodord-reservation-preorder-decline><?php esc_html_e( 'No, continue without pre-order', 'foodord' ); ?></button>
									</div>
								</div>
								<?php $this->render_preorder_section( $preorder_products, $form_control_ids ); ?>
							<?php endif; ?>

						</form>
					<?php endif; ?>
				</div>
			</div>
		</div>
		<?php

		return (string) ob_get_clean();
	}

	/**
	 * Render the reservations empty state when no tables are configured.
	 *
	 * @param bool   $can_manage_reservations Whether the current user can manage reservations.
	 * @param string $reservations_admin_url  Reservations admin page URL.
	 */
	private function render_reservations_empty_state( bool $can_manage_reservations, string $reservations_admin_url ): void {
		$is_admin_setup = $can_manage_reservations && '' !== $reservations_admin_url;
		$empty_class    = 'foodord-table-reservations__empty' . ( $is_admin_setup ? ' foodord-table-reservations__empty--admin' : ' foodord-table-reservations__empty--public' );
		?>
		<div class="<?php echo esc_attr( $empty_class ); ?>" role="status">
			<div class="foodord-table-reservations__empty-panel">
				<span class="foodord-table-reservations__empty-icon" aria-hidden="true">
					<svg class="foodord-table-reservations__empty-icon-graphic" viewBox="0 0 24 24" focusable="false">
						<path d="M8 2v4"></path>
						<path d="M16 2v4"></path>
						<rect x="3" y="4" width="18" height="18" rx="2"></rect>
						<path d="M3 10h18"></path>
						<path d="m9 16 2 2 4-4"></path>
					</svg>
				</span>
				<?php if ( $is_admin_setup ) : ?>
					<h2 class="foodord-table-reservations__empty-title"><?php esc_html_e( 'No reservation tables have been added yet', 'foodord' ); ?></h2>
					<p class="foodord-table-reservations__empty-text"><?php esc_html_e( 'Add tables to make online reservations available.', 'foodord' ); ?></p>
					<form class="foodord-table-reservations__empty-action" method="get" action="<?php echo esc_url( admin_url( 'admin.php' ) ); ?>">
						<input class="foodord-table-reservations__hidden-field" type="hidden" name="page" value="<?php echo esc_attr( $this->get_reservations_admin_page_slug() ); ?>" />
							<button class="foodord-table-reservations__button foodord-table-reservations__button--primary foodord-table-reservations__empty-button" type="submit">
								<span><?php esc_html_e( 'Open Reservations settings', 'foodord' ); ?></span>
								<svg class="foodord-table-reservations__button-icon foodord-table-reservations__empty-button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
									<path d="M12 15.5a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7Z"></path>
									<path d="M19.4 15a1.8 1.8 0 0 0 .36 1.98l.06.06a2.1 2.1 0 1 1-2.97 2.97l-.06-.06a1.8 1.8 0 0 0-1.98-.36 1.8 1.8 0 0 0-1.08 1.65V21a2.1 2.1 0 1 1-4.2 0v-.09a1.8 1.8 0 0 0-1.08-1.65 1.8 1.8 0 0 0-1.98.36l-.06.06a2.1 2.1 0 1 1-2.97-2.97l.06-.06A1.8 1.8 0 0 0 4.6 15a1.8 1.8 0 0 0-1.65-1.08H3a2.1 2.1 0 1 1 0-4.2h.09A1.8 1.8 0 0 0 4.74 8.64a1.8 1.8 0 0 0-.36-1.98l-.06-.06a2.1 2.1 0 1 1 2.97-2.97l.06.06a1.8 1.8 0 0 0 1.98.36A1.8 1.8 0 0 0 10.41 2.4V2.1a2.1 2.1 0 1 1 4.2 0v.09a1.8 1.8 0 0 0 1.08 1.65 1.8 1.8 0 0 0 1.98-.36l.06-.06a2.1 2.1 0 1 1 2.97 2.97l-.06.06a1.8 1.8 0 0 0-.36 1.98c.27.68.93 1.13 1.65 1.13H22a2.1 2.1 0 1 1 0 4.2h-.09A1.8 1.8 0 0 0 19.4 15Z"></path>
								</svg>
							</button>
					</form>
				<?php else : ?>
					<h2 class="foodord-table-reservations__empty-title"><?php esc_html_e( 'Online reservations are not available yet', 'foodord' ); ?></h2>
					<p class="foodord-table-reservations__empty-text"><?php esc_html_e( 'Please check back later.', 'foodord' ); ?></p>
				<?php endif; ?>
			</div>
		</div>
		<?php
	}

	/**
	 * Render the reservation state shown when the monthly allowance is exhausted.
	 *
	 * @param bool   $can_manage_reservations Whether the current user can manage reservations.
	 * @param string $license_admin_url       License admin page URL.
	 */
	private function render_reservation_quota_empty_state( bool $can_manage_reservations, string $license_admin_url ): void {
		$is_admin_view = $can_manage_reservations && '' !== $license_admin_url;
		$empty_class   = 'foodord-table-reservations__empty ' . ( $is_admin_view ? 'foodord-table-reservations__empty--admin' : 'foodord-table-reservations__empty--public' );
		$license_page  = function_exists( 'foodord_get_settings_page_slug' ) ? foodord_get_settings_page_slug( 'license' ) : 'foodord-license';
		?>
		<div class="<?php echo esc_attr( $empty_class ); ?>" role="status">
			<div class="foodord-table-reservations__empty-panel">
				<span class="foodord-table-reservations__empty-icon" aria-hidden="true">
					<svg class="foodord-table-reservations__empty-icon-graphic" viewBox="0 0 24 24" focusable="false">
						<path d="M8 2v4"></path>
						<path d="M16 2v4"></path>
						<rect x="3" y="4" width="18" height="18" rx="2"></rect>
						<path d="M3 10h18"></path>
						<path d="m9 15 6 6"></path>
						<path d="m15 15-6 6"></path>
					</svg>
				</span>
				<?php if ( $is_admin_view ) : ?>
					<h2 class="foodord-table-reservations__empty-title"><?php esc_html_e( 'Monthly reservation limit reached', 'foodord' ); ?></h2>
					<p class="foodord-table-reservations__empty-text"><?php esc_html_e( 'Activate a FoodOrd license to continue accepting online reservations.', 'foodord' ); ?></p>
					<form class="foodord-table-reservations__empty-action" method="get" action="<?php echo esc_url( admin_url( 'admin.php' ) ); ?>">
						<input class="foodord-table-reservations__hidden-field" type="hidden" name="page" value="<?php echo esc_attr( $license_page ); ?>" />
						<button class="foodord-table-reservations__button foodord-table-reservations__button--primary foodord-table-reservations__empty-button" type="submit">
							<span><?php esc_html_e( 'Open License settings', 'foodord' ); ?></span>
						</button>
					</form>
				<?php else : ?>
					<h2 class="foodord-table-reservations__empty-title"><?php esc_html_e( 'Online reservations are currently unavailable', 'foodord' ); ?></h2>
					<p class="foodord-table-reservations__empty-text"><?php esc_html_e( 'Please contact the restaurant directly.', 'foodord' ); ?></p>
				<?php endif; ?>
			</div>
		</div>
		<?php
	}

	/**
	 * Render the reservation progress timeline.
	 *
	 * @param bool $include_preorder Whether to include the pre-order step.
	 */
	private function render_reservation_timeline( bool $include_preorder = true ): void {
		$preorder_enabled = $include_preorder && Foodord_Table_Reservation_Preorders::get_settings()['enabled'];
		?>
		<div class="foodord-table-reservations__timeline" aria-label="<?php echo esc_attr__( 'Reservation steps', 'foodord' ); ?>">
			<ol class="foodord-table-reservations__timeline-list">
				<li class="foodord-table-reservations__timeline-item" data-foodord-reservation-step-item="time">
					<button class="foodord-table-reservations__timeline-marker" type="button" data-foodord-reservation-back aria-label="<?php echo esc_attr__( 'Choose time', 'foodord' ); ?>" disabled>1</button>
					<span class="foodord-table-reservations__timeline-label"><?php esc_html_e( 'Choose time', 'foodord' ); ?></span>
				</li>
				<li class="foodord-table-reservations__timeline-item" data-foodord-reservation-step-item="contact">
					<span class="foodord-table-reservations__timeline-marker" aria-hidden="true">2</span>
					<span class="foodord-table-reservations__timeline-label"><?php esc_html_e( 'Your details', 'foodord' ); ?></span>
				</li>
				<?php if ( $preorder_enabled ) : ?>
					<li class="foodord-table-reservations__timeline-item" data-foodord-reservation-step-item="preorder">
						<span class="foodord-table-reservations__timeline-marker" aria-hidden="true">3</span>
						<span class="foodord-table-reservations__timeline-label"><?php esc_html_e( 'Pre-order', 'foodord' ); ?></span>
					</li>
				<?php endif; ?>
				<li class="foodord-table-reservations__timeline-item" data-foodord-reservation-step-item="done">
					<span class="foodord-table-reservations__timeline-marker" aria-hidden="true" data-foodord-reservation-done-marker data-default-step="3" data-preorder-step="4">3</span>
					<span class="foodord-table-reservations__timeline-label"><?php esc_html_e( 'Done', 'foodord' ); ?></span>
				</li>
			</ol>
		</div>
		<?php
	}

	/**
	 * Group pre-order products by their primary WooCommerce category.
	 *
	 * @param WC_Product[] $products Pre-order products.
	 * @return array<string,array{name:string,description:string,products:WC_Product[]}>
	 */
	private function group_preorder_products_by_category( array $products ): array {
		$categories     = get_terms(
			array(
				'taxonomy'   => 'product_cat',
				'hide_empty' => false,
				'orderby'    => 'menu_order',
				'order'      => 'ASC',
			)
		);
		$category_order = array();

		if ( is_array( $categories ) ) {
			foreach ( $categories as $index => $category ) {
				if ( $category instanceof WP_Term ) {
					$category_order[ (string) $category->term_id ] = $index;
				}
			}
		}

		$product_ids   = array_values(
			array_map(
				static function ( WC_Product $product ): int {
					return $product->get_id();
				},
				array_filter(
					$products,
					static function ( $product ): bool {
						return $product instanceof WC_Product;
					}
				)
			)
		);
		$product_terms = ! empty( $product_ids )
			? wp_get_object_terms(
				$product_ids,
				'product_cat',
				array(
					'orderby' => 'menu_order',
					'order'   => 'ASC',
					'fields'  => 'all_with_object_id',
				)
			)
			: array();
		$primary_terms = array();
		if ( is_array( $product_terms ) ) {
			foreach ( $product_terms as $product_term ) {
				$product_id = isset( $product_term->object_id ) ? absint( $product_term->object_id ) : 0;
				if ( $product_id > 0 && $product_term instanceof WP_Term && ! isset( $primary_terms[ $product_id ] ) ) {
					$primary_terms[ $product_id ] = $product_term;
				}
			}
		}

		$groups = array();
		foreach ( $products as $product ) {
			if ( ! $product instanceof WC_Product ) {
				continue;
			}

			$primary_term = $primary_terms[ $product->get_id() ] ?? null;
			$category_key = $primary_term ? (string) $primary_term->term_id : 'uncategorized';

			if ( ! isset( $groups[ $category_key ] ) ) {
				$category_description    = $primary_term ? term_description( (int) $primary_term->term_id ) : '';
				$category_description    = is_string( $category_description ) ? trim( $category_description ) : '';
				$groups[ $category_key ] = array(
					'name'        => $primary_term ? $primary_term->name : __( 'Uncategorized', 'foodord' ),
					'description' => $category_description,
					'products'    => array(),
				);
			}

			$groups[ $category_key ]['products'][] = $product;
		}

		uksort(
			$groups,
			static function ( string $first_key, string $second_key ) use ( $category_order ): int {
				$first_order  = $category_order[ $first_key ] ?? PHP_INT_MAX;
				$second_order = $category_order[ $second_key ] ?? PHP_INT_MAX;

				return $first_order === $second_order
					? strcmp( $first_key, $second_key )
					: $first_order <=> $second_order;
			}
		);

		return $groups;
	}

	/**
	 * Render the allergy information trigger for a pre-order product card.
	 *
	 * @param int  $product_id Product ID.
	 * @param bool $has_product_allergens Whether product-specific allergens exist.
	 * @param bool $allergy_info_active Whether global allergy information is available.
	 */
	private function render_preorder_allergy_trigger( int $product_id, bool $has_product_allergens, bool $allergy_info_active ): void {
		if ( ! $has_product_allergens && ! $allergy_info_active ) {
			return;
		}
		?>
		<button type="button" class="foodord-allergy-trigger" aria-label="<?php echo esc_attr__( 'Open Allergy Info', 'foodord' ); ?>" data-allergy-trigger="true" data-product-id="<?php echo esc_attr( $product_id ); ?>" data-has-product-allergens="<?php echo esc_attr( $has_product_allergens ? '1' : '0' ); ?>">
			<span class="foodord-allergy-trigger__icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg></span>
		</button>
		<?php
	}

	/**
	 * Render the reservation pre-order product and summary step.
	 *
	 * @param WC_Product[]         $products         Pre-order products.
	 * @param array<string,string> $form_control_ids Unique form control IDs.
	 */
	private function render_preorder_section( array $products, array $form_control_ids ): void {
		$product_groups      = $this->group_preorder_products_by_category( $products );
		$first_category_key  = (string) array_key_first( $product_groups );
		$allergy_info_active = class_exists( 'Foodord_Allergy_Info' ) ? Foodord_Allergy_Info::is_enabled() && Foodord_Allergy_Info::has_content() : false;
		?>
		<div class="foodord-table-reservations__section foodord-table-reservations__preorder" data-foodord-reservation-preorder hidden>
			<div class="foodord-table-reservations__preorder-heading">
				<h3 class="foodord-table-reservations__preorder-title"><?php esc_html_e( 'Choose your food', 'foodord' ); ?></h3>
				<p class="foodord-table-reservations__preorder-description"><?php esc_html_e( 'Add the items you would like prepared for your reservation.', 'foodord' ); ?></p>
			</div>
			<?php if ( count( $product_groups ) > 1 ) : ?>
				<div class="foodord-table-reservations__preorder-navigation-layout">
					<nav class="foodord-table-reservations__preorder-navigation" aria-label="<?php echo esc_attr__( 'Pre-order categories', 'foodord' ); ?>" data-foodord-reservation-preorder-navigation>
						<?php foreach ( $product_groups as $category_key => $category_data ) : ?>
							<?php $is_active_category = $first_category_key === $category_key; ?>
							<button class="foodord-table-reservations__preorder-navigation-button<?php echo $is_active_category ? ' is-active' : ''; ?>" type="button" data-foodord-reservation-preorder-category-button="<?php echo esc_attr( $category_key ); ?>" aria-current="<?php echo esc_attr( $is_active_category ? 'true' : 'false' ); ?>"><?php echo esc_html( $category_data['name'] ); ?></button>
						<?php endforeach; ?>
					</nav>
				</div>
			<?php endif; ?>
			<div class="foodord-table-reservations__preorder-layout">
				<div class="foodord-table-reservations__preorder-products" data-foodord-reservation-preorder-products>
					<?php if ( empty( $product_groups ) ) : ?>
						<p class="foodord-table-reservations__preorder-empty"><?php esc_html_e( 'No products are available for pre-order right now.', 'foodord' ); ?></p>
					<?php else : ?>
						<?php foreach ( $product_groups as $category_key => $category_data ) : ?>
							<?php
							$has_category_description = '' !== $category_data['description'];
							$category_state           = $has_category_description ? 'has-description' : 'no-description';
							?>
							<section class="foodord-table-reservations__preorder-category foodord-table-reservations__preorder-category--<?php echo esc_attr( $category_state ); ?>" data-foodord-reservation-preorder-category="<?php echo esc_attr( $category_key ); ?>">
								<h4 class="foodord-table-reservations__preorder-category-title foodord-product-category--<?php echo esc_attr( $category_state ); ?>"><?php echo esc_html( $category_data['name'] ); ?></h4>
								<?php if ( '' !== $category_data['description'] ) : ?>
									<div class="foodord-table-reservations__preorder-category-description"><?php echo wp_kses_post( $category_data['description'] ); ?></div>
								<?php endif; ?>
								<div class="foodord-table-reservations__preorder-category-products">
									<?php foreach ( $category_data['products'] as $product ) : ?>
										<?php
										$product_id            = (int) $product->get_id();
										$image_url             = wp_get_attachment_image_url( (int) $product->get_image_id(), 'woocommerce_thumbnail' );
										$has_product_image     = is_string( $image_url ) && '' !== $image_url;
										$has_product_allergens = class_exists( 'Foodord_Product_Data_Allergens' ) ? Foodord_Product_Data_Allergens::has_product_allergens( $product_id ) : false;
										$product_labels_html   = class_exists( 'Foodord_Product_Labels' ) ? wp_kses( Foodord_Product_Labels::get_product_labels_html( $product_id, 'card' ), Foodord_Product_Labels::get_allowed_html() ) : '';
										$product_description   = Foodord_Products_Renderer::get_formatted_description( $product );
										Foodord_Table_Reservation_Preorders::begin_pricing_rules_bypass();
										try {
											$price_html = Foodord_Products_Renderer::get_product_price_html( $product );
										} finally {
											Foodord_Table_Reservation_Preorders::end_pricing_rules_bypass();
										}
										$product_card_classes = 'foodord-product-card foodord-table-reservations__preorder-product';
										if ( ! $has_product_image ) {
											$product_card_classes .= ' foodord-product-card--no-image';
										}
										if ( '' !== $product_labels_html ) {
											$product_card_classes .= ' foodord-product-card--has-labels';
										}
										?>
										<article class="<?php echo esc_attr( $product_card_classes ); ?>" data-product-id="<?php echo esc_attr( $product_id ); ?>" data-foodord-available="1" data-foodord-direct-add="0">
											<?php if ( $has_product_image ) : ?>
												<div class="product-image-container">
													<div class="product-image-wrapper">
														<img class="product-image foodord-table-reservations__preorder-product-image" src="<?php echo esc_url( $image_url ); ?>" alt="" loading="lazy" />
													</div>
												</div>
											<?php endif; ?>
											<div class="foodord-table-reservations__preorder-product-content">
												<div class="foodord-table-reservations__preorder-product-title-row">
													<h5 class="foodord-table-reservations__preorder-product-title"><?php echo esc_html( $product->get_name() ); ?></h5>
													<?php $this->render_preorder_allergy_trigger( $product_id, $has_product_allergens, $allergy_info_active ); ?>
												</div>
												<?php if ( '' !== $product_description ) : ?>
													<div class="foodord-table-reservations__preorder-product-description product-description">
														<p class="product-description-text"><?php echo wp_kses( $product_description, array( 'br' => array() ) ); ?></p>
													</div>
												<?php endif; ?>
												<div class="foodord-table-reservations__preorder-product-price"><?php echo wp_kses_post( $price_html ); ?></div>
												<?php if ( '' !== $product_labels_html ) : ?>
													<?php echo wp_kses( $product_labels_html, Foodord_Product_Labels::get_allowed_html() ); ?>
												<?php endif; ?>
											</div>
											<div class="product-actions">
												<?php /* translators: %s: Product name. */ ?>
												<button class="button add_to_cart_button foodord-table-reservations__preorder-product-add" type="button" aria-label="<?php echo esc_attr( sprintf( __( 'Add %s', 'foodord' ), $product->get_name() ) ); ?>" data-foodord-direct-add="0">
													<span class="screen-reader-text"><?php esc_html_e( 'Add to cart', 'foodord' ); ?></span>
												</button>
											</div>
										</article>
									<?php endforeach; ?>
								</div>
							</section>
						<?php endforeach; ?>
						<div class="foodord-table-reservations__preorder-products-end">
							<button class="foodord-table-reservations__preorder-products-back-to-top" type="button" data-foodord-reservation-preorder-back-to-top aria-label="<?php echo esc_attr__( 'Back to top', 'foodord' ); ?>">
								<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5"></path><path d="m6 11 6-6 6 6"></path></svg>
							</button>
							<strong><?php esc_html_e( 'Need anything else?', 'foodord' ); ?></strong>
							<p class="foodord-table-reservations__preorder-products-end-copy"><?php esc_html_e( "You've reached the end of the menu.", 'foodord' ); ?></p>
							<span><?php esc_html_e( 'Review the menu again or continue with your pre-order.', 'foodord' ); ?></span>
						</div>
					<?php endif; ?>
				</div>
				<div class="foodord-table-reservations__preorder-sidebar" data-foodord-reservation-preorder-mobile-cart>
					<button class="foodord-table-reservations__preorder-mobile-backdrop" type="button" data-foodord-reservation-preorder-mobile-cart-close aria-label="<?php echo esc_attr__( 'Close cart', 'foodord' ); ?>"></button>
					<div class="foodord-table-reservations__preorder-summary-stack">
						<aside class="foodord-table-reservations__preorder-summary" aria-live="polite">
							<div class="foodord-table-reservations__preorder-summary-header">
								<h3 id="<?php echo esc_attr( $form_control_ids['preorder_cart_title'] ); ?>" class="foodord-table-reservations__preorder-summary-title"><?php esc_html_e( 'Your pre-order', 'foodord' ); ?></h3>
								<div class="foodord-table-reservations__preorder-summary-actions">
									<button class="foodord-table-reservations__preorder-clear" type="button" data-foodord-reservation-preorder-clear title="<?php echo esc_attr__( 'Clear cart', 'foodord' ); ?>" aria-label="<?php echo esc_attr__( 'Clear all items from cart', 'foodord' ); ?>" disabled>
										<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
											<path d="M3 6h18"></path>
											<path d="M8 6V4h8v2"></path>
											<path d="M19 6l-1 14H6L5 6"></path>
											<path d="M10 11v5"></path>
											<path d="M14 11v5"></path>
										</svg>
									</button>
									<button class="foodord-table-reservations__preorder-mobile-close" type="button" data-foodord-reservation-preorder-mobile-cart-close aria-label="<?php echo esc_attr__( 'Close cart', 'foodord' ); ?>">
										<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
											<path d="m18 6-12 12"></path>
											<path d="m6 6 12 12"></path>
										</svg>
									</button>
								</div>
							</div>
							<div class="foodord-table-reservations__preorder-items" data-foodord-reservation-preorder-items></div>
							<p class="foodord-table-reservations__preorder-summary-empty" data-foodord-reservation-preorder-empty><?php esc_html_e( 'No items added yet.', 'foodord' ); ?></p>
							<div class="foodord-table-reservations__preorder-total" data-foodord-reservation-preorder-total hidden>
								<span><?php esc_html_e( 'Total', 'foodord' ); ?></span>
								<strong data-foodord-reservation-preorder-total-value></strong>
							</div>
						</aside>
						<div class="foodord-table-reservations__message foodord-table-reservations__preorder-mobile-message" data-foodord-reservation-preorder-message role="status" aria-live="polite"></div>
						<button class="foodord-table-reservations__button foodord-table-reservations__button--primary foodord-table-reservations__preorder-checkout" type="button" data-foodord-reservation-preorder-checkout disabled><?php esc_html_e( 'Continue to payment', 'foodord' ); ?></button>
					</div>
					<button class="foodord-table-reservations__preorder-cancel-reservation" type="button" data-foodord-reservation-preorder-leave><?php esc_html_e( 'Cancel reservation', 'foodord' ); ?></button>
				</div>
			</div>
			<button class="foodord-table-reservations__preorder-mobile-trigger" type="button" data-foodord-reservation-preorder-mobile-cart-trigger aria-expanded="false" hidden>
				<span class="foodord-table-reservations__preorder-mobile-trigger-info">
					<span class="foodord-table-reservations__preorder-mobile-count" data-foodord-reservation-preorder-mobile-cart-count aria-label="<?php echo esc_attr__( 'Items in cart', 'foodord' ); ?>">0</span>
					<span><?php esc_html_e( 'View Cart', 'foodord' ); ?></span>
				</span>
				<strong data-foodord-reservation-preorder-mobile-cart-total></strong>
			</button>
			<div class="foodord-table-reservations__message" data-foodord-reservation-preorder-message role="status" aria-live="polite"></div>
			<div class="foodord-table-reservations__confirm" data-foodord-reservation-preorder-clear-modal hidden>
				<div class="foodord-table-reservations__confirm-backdrop" data-foodord-reservation-preorder-clear-close></div>
				<div class="foodord-table-reservations__confirm-dialog" role="dialog" aria-modal="true" aria-labelledby="<?php echo esc_attr( $form_control_ids['preorder_clear_title'] ); ?>">
					<h3 id="<?php echo esc_attr( $form_control_ids['preorder_clear_title'] ); ?>" class="foodord-table-reservations__confirm-title"><?php esc_html_e( 'Clear Cart', 'foodord' ); ?></h3>
					<p class="foodord-table-reservations__confirm-text"><?php esc_html_e( 'Are you sure you want to clear all items from your cart?', 'foodord' ); ?></p>
					<div class="foodord-table-reservations__confirm-actions">
						<button class="foodord-table-reservations__button foodord-table-reservations__confirm-button foodord-table-reservations__confirm-button--secondary" type="button" data-foodord-reservation-preorder-clear-close><?php esc_html_e( 'Cancel', 'foodord' ); ?></button>
						<button class="foodord-table-reservations__button foodord-table-reservations__confirm-button foodord-table-reservations__confirm-button--danger" type="button" data-foodord-reservation-preorder-clear-confirm><?php esc_html_e( 'Clear Cart', 'foodord' ); ?></button>
					</div>
				</div>
			</div>
			<div class="foodord-table-reservations__confirm" data-foodord-reservation-preorder-leave-modal hidden>
				<div class="foodord-table-reservations__confirm-backdrop" data-foodord-reservation-preorder-leave-close></div>
				<div class="foodord-table-reservations__confirm-dialog" role="dialog" aria-modal="true" aria-labelledby="<?php echo esc_attr( $form_control_ids['preorder_leave_title'] ); ?>">
					<h3 id="<?php echo esc_attr( $form_control_ids['preorder_leave_title'] ); ?>" class="foodord-table-reservations__confirm-title"><?php esc_html_e( 'Cancel reservation?', 'foodord' ); ?></h3>
					<p class="foodord-table-reservations__confirm-text"><?php esc_html_e( 'Your table hold and selected food will be removed. This action cannot be undone.', 'foodord' ); ?></p>
					<div class="foodord-table-reservations__confirm-actions">
						<button class="foodord-table-reservations__button foodord-table-reservations__confirm-button foodord-table-reservations__confirm-button--secondary" type="button" data-foodord-reservation-preorder-leave-close><?php esc_html_e( 'Keep reservation', 'foodord' ); ?></button>
						<button class="foodord-table-reservations__button foodord-table-reservations__confirm-button foodord-table-reservations__confirm-button--danger" type="button" data-foodord-reservation-preorder-leave-confirm><?php esc_html_e( 'Cancel reservation', 'foodord' ); ?></button>
					</div>
				</div>
			</div>
		</div>
		<?php
	}

	/**
	 * Get success screen data after a paid reservation deposit redirect.
	 *
	 * @return array<string,mixed>
	 */
	private function get_deposit_success_payload(): array {
		if ( ! class_exists( 'Foodord_Table_Reservation_Deposits' ) ) {
			return array();
		}

		$reservation = Foodord_Table_Reservation_Deposits::get_paid_success_reservation_from_request( $this->reservations );
		if ( ! $reservation ) {
			return array();
		}

		$status       = isset( $reservation->status ) ? Foodord_Table_Reservations::sanitize_status( (string) $reservation->status ) : 'pending';
		$is_confirmed = 'confirmed' === $status;
		$is_preorder  = Foodord_Table_Reservation_Deposits::reservation_has_preorder( $reservation );

		return array(
			'reservationCode' => isset( $reservation->reservation_code ) ? (string) $reservation->reservation_code : '',
			'isPreorder'      => $is_preorder,
			'status'          => $status,
			'title'           => $is_confirmed ? __( 'Reservation confirmed', 'foodord' ) : __( 'Reservation request received', 'foodord' ),
			'message'         => $is_preorder
				? ( $is_confirmed
					? __( 'Your payment is complete and your reservation pre-order is confirmed. We have sent the details to your email address.', 'foodord' )
					: __( 'Your payment is complete. Your reservation and pre-order request have been received. We will review your request and notify you by email once it has been confirmed.', 'foodord' ) )
				: ( $is_confirmed
					? __( 'Your deposit has been paid and your reservation is confirmed. We have sent the details to your email address.', 'foodord' )
					: __( 'Your deposit has been paid. Your reservation request is awaiting confirmation.', 'foodord' ) ),
		);
	}

	/**
	 * Resolve the signed context used to edit an unpaid pre-order.
	 *
	 * @return array<string,mixed>
	 */
	private function get_preorder_edit_context(): array {
		if ( ! class_exists( 'Foodord_Table_Reservation_Deposits' ) ) {
			return array();
		}

		$order_id = absint( filter_input( INPUT_GET, 'foodord_reservation_preorder_edit', FILTER_SANITIZE_NUMBER_INT ) );
		$edit_key = $this->get_query_text_field( 'foodord_reservation_preorder_key' );
		$order    = Foodord_Table_Reservation_Deposits::get_editable_preorder_order( $order_id, $edit_key );
		if ( ! $order instanceof WC_Order ) {
			return array();
		}

		$draft       = $this->preorders->get_payment_edit_draft_by_order( $order_id );
		$reservation = $draft ? $this->reservations->get_reservation( absint( $draft->reservation_id ) ) : null;
		if ( ! $draft || ! $reservation || absint( $draft->reservation_id ) !== absint( $reservation->id ) ) {
			return array();
		}

		return array(
			'draftToken'   => (string) $draft->draft_token,
			'expiresAtGmt' => Foodord_Table_Reservation_Deposits::get_payment_expiry_gmt( $order ),
			'fields'       => array(
				'date'            => (string) $reservation->reservation_date,
				'time'            => (string) $reservation->reservation_time,
				'party_size'      => (int) $reservation->party_size,
				'name'            => (string) $reservation->customer_name,
				'email'           => (string) $reservation->customer_email,
				'phone'           => (string) $reservation->customer_phone,
				'note'            => (string) $reservation->customer_note,
				'privacy_consent' => ! empty( $reservation->privacy_consent ) ? '1' : '',
			),
		);
	}

	/**
	 * Get the reservation banner image URL.
	 */
	private function get_banner_image_url(): string {
		if ( $this->is_banner_disabled() ) {
			return '';
		}

		$image_id = absint( get_option( self::OPTION_BANNER_IMAGE_ID, 0 ) );
		if ( $image_id > 0 && wp_attachment_is_image( $image_id ) ) {
			$image_url = wp_get_attachment_image_url( $image_id, 'large' );
			if ( is_string( $image_url ) && '' !== $image_url ) {
				return $image_url;
			}
		}

		return FOODORD_URL . 'public/images/table-reservations-banner.png';
	}

	/**
	 * Check whether the reservation banner is disabled.
	 */
	private function is_banner_disabled(): bool {
		return '1' === (string) get_option( self::OPTION_BANNER_DISABLED, '1' );
	}

	/**
	 * Get the Reservations admin page slug.
	 */
	private function get_reservations_admin_page_slug(): string {
		return function_exists( 'foodord_get_settings_page_slug' )
			? foodord_get_settings_page_slug( 'reservations' )
			: 'foodord-reservations';
	}

	/**
	 * Get a readable date placeholder based on the WordPress date format.
	 */
	private function get_date_placeholder(): string {
		return Foodord_Table_Reservations::get_date_placeholder();
	}

	/**
	 * Get the jQuery UI date format matching the WordPress date format.
	 */
	private function get_jquery_date_format(): string {
		return Foodord_Table_Reservations::get_jquery_datepicker_format();
	}

	/**
	 * Get frontend datepicker disabled rules.
	 *
	 * @return array{weekdays:array<int,int>,ranges:array<int,array{from:string,to:string}>}
	 */
	private function get_datepicker_disabled_rules(): array {
		return Foodord_Table_Reservations::get_datepicker_disabled_rules();
	}

	/**
	 * Get time slots for the selected date.
	 */
	public function ajax_get_time_slots(): void {
		$this->verify_request();

		$date                  = $this->get_post_text_field( 'date' );
		$party_size            = absint( filter_input( INPUT_POST, 'party_size', FILTER_SANITIZE_NUMBER_INT ) );
		$slots                 = $this->reservations->get_operational_time_options_for_date( $date, $party_size );
		$cutoff_blocks_date    = $this->preorder_cutoff_blocks_all_times( $slots, $party_size );
		$has_preorder_products = ! empty( $this->preorders->get_catalog_products() );
		$slots                 = array_values(
			array_filter(
				$slots,
				static function ( array $slot ) use ( $party_size, $has_preorder_products ): bool {
					if ( ! Foodord_Table_Reservation_Preorders::is_required_for_slot( $slot['date'], $slot['time'], $party_size ) ) {
						return true;
					}

					return $has_preorder_products && Foodord_Table_Reservation_Preorders::cutoff_is_satisfied( $slot['date'], $slot['time'] );
				}
			)
		);
		$times                 = array_column( $slots, 'time' );

		wp_send_json_success(
			array(
				'times'       => $times,
				'slots'       => $slots,
				'time_labels' => $this->get_time_slot_labels( $times ),
				'message'     => $cutoff_blocks_date
					? __( 'Reservations are unavailable for the selected date. Please choose another date.', 'foodord' )
					: $this->get_time_slots_unavailable_message( $times, $party_size ),
			)
		);
	}

	/**
	 * Check whether every available time is blocked by the required pre-order cutoff.
	 *
	 * @param array<int,array{date:string,time:string}> $slots      Reservation slots before pre-order filtering.
	 * @param int                                       $party_size Requested party size.
	 */
	private function preorder_cutoff_blocks_all_times( array $slots, int $party_size ): bool {
		if ( empty( $slots ) ) {
			return false;
		}

		foreach ( $slots as $slot ) {
			if (
				! Foodord_Table_Reservation_Preorders::is_required_for_slot( $slot['date'], $slot['time'], $party_size )
				|| Foodord_Table_Reservation_Preorders::cutoff_is_satisfied( $slot['date'], $slot['time'] )
			) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Get floor-plan tables available for the selected reservation slot.
	 */
	public function ajax_get_selectable_tables(): void {
		$this->verify_request();
		$this->verify_request_ip_allowed();

		$result = $this->reservations->get_selectable_tables_for_slot(
			$this->get_post_text_field( 'date' ),
			$this->get_post_text_field( 'time' ),
			absint( filter_input( INPUT_POST, 'party_size', FILTER_SANITIZE_NUMBER_INT ) )
		);
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		if ( empty( $result ) ) {
			wp_send_json_error( array( 'message' => __( 'No tables are available for the selected time.', 'foodord' ) ) );
		}

		wp_send_json_success( array( 'floors' => $result ) );
	}

	/**
	 * Get available time slots for a customer-managed reservation.
	 */
	public function ajax_get_manage_time_slots(): void {
		$this->verify_request();

		$reservation_id = absint( filter_input( INPUT_POST, 'reservation_id', FILTER_SANITIZE_NUMBER_INT ) );
		$token          = $this->get_post_text_field( 'token' );
		$reservation    = $this->reservations->get_reservation_by_manage_token( $reservation_id, $token );
		if ( is_wp_error( $reservation ) ) {
			wp_send_json_error( array( 'message' => $reservation->get_error_message() ), 403 );
		}

		if ( ! $this->reservations->can_customer_manage_reservation( $reservation ) ) {
			wp_send_json_error( array( 'message' => __( 'This reservation cannot be changed right now. Please contact the restaurant.', 'foodord' ) ), 403 );
		}

		$date       = $this->get_post_text_field( 'date' );
		$party_size = max( 1, absint( filter_input( INPUT_POST, 'party_size', FILTER_SANITIZE_NUMBER_INT ) ) );
		$times      = $this->reservations->get_time_options_for_date( $date, $party_size, (int) $reservation->id );

		wp_send_json_success(
			array(
				'times'       => $times,
				'time_labels' => $this->get_time_slot_labels( $times ),
				'message'     => $this->get_time_slots_unavailable_message( $times, $party_size ),
			)
		);
	}

	/**
	 * Format reservation time options using the WordPress time format.
	 *
	 * @param string[] $times Time values in H:i format.
	 * @return array<string,string>
	 */
	private function get_time_slot_labels( array $times ): array {
		$labels      = array();
		$time_format = (string) get_option( 'time_format', 'H:i' );

		foreach ( $times as $time ) {
			$time      = substr( sanitize_text_field( (string) $time ), 0, 5 );
			$date_time = DateTimeImmutable::createFromFormat( '!H:i', $time, wp_timezone() );
			$errors    = DateTimeImmutable::getLastErrors();

			if (
				! $date_time instanceof DateTimeImmutable ||
				( is_array( $errors ) && ( $errors['warning_count'] > 0 || $errors['error_count'] > 0 ) ) ||
				$date_time->format( 'H:i' ) !== $time
			) {
				continue;
			}

			$labels[ $time ] = wp_date( $time_format, $date_time->getTimestamp(), wp_timezone() );
		}

		return $labels;
	}

	/**
	 * Get the appropriate message when no reservation times are available.
	 *
	 * @param string[] $times      Available reservation times.
	 * @param int      $party_size Requested party size.
	 */
	private function get_time_slots_unavailable_message( array $times, int $party_size ): string {
		if ( ! empty( $times ) ) {
			return '';
		}

		$capacity_validation = $this->reservations->validate_party_size_capacity( $party_size );
		if ( is_wp_error( $capacity_validation ) ) {
			return $capacity_validation->get_error_message();
		}

		return __( 'Reservations are not available on this date. Please choose another date.', 'foodord' );
	}

	/**
	 * Check availability and create a hard hold only when payment may follow.
	 */
	public function ajax_create_hold(): void {
		$this->verify_request();
		$this->verify_request_ip_allowed();
		$this->verify_recaptcha_request( self::RECAPTCHA_ACTION_HOLD );

		if ( class_exists( 'Foodord_Reservation_Quota' ) ) {
			$quota_status = Foodord_Reservation_Quota::get_status();
			if ( ! empty( $quota_status['known'] ) && empty( $quota_status['allowed'] ) ) {
				wp_send_json_error( array( 'message' => Foodord_Reservation_Quota::get_customer_limit_message() ), 403 );
			}
		}

		$date              = $this->get_post_text_field( 'date' );
		$preorder_date     = Foodord_Table_Reservations::normalize_date_input( $date );
		$time              = $this->get_post_text_field( 'time' );
		$party_size        = absint( filter_input( INPUT_POST, 'party_size', FILTER_SANITIZE_NUMBER_INT ) );
		$table_id          = absint( filter_input( INPUT_POST, 'table_id', FILTER_SANITIZE_NUMBER_INT ) );
		$customer_session  = $this->get_post_text_field( 'customer_session' );
		$preorder_required = Foodord_Table_Reservation_Preorders::is_required_for_slot( $preorder_date, $time, $party_size );
		if ( $preorder_required && ! Foodord_Table_Reservation_Preorders::cutoff_is_satisfied( $preorder_date, $time ) ) {
			wp_send_json_error( array( 'message' => __( 'The pre-order cutoff has passed for this reservation time. Please choose another time.', 'foodord' ) ) );
		}
		$preorder_available = Foodord_Table_Reservation_Preorders::is_available_for_slot( $preorder_date, $time, $party_size )
			&& ! empty( $this->preorders->get_catalog_products() );
		if ( $preorder_required && ! $preorder_available ) {
			wp_send_json_error( array( 'message' => __( 'Pre-ordering is not available for this reservation time. Please choose another time.', 'foodord' ) ) );
		}

		if ( $table_id > 0 && ! Foodord_Table_Reservations::is_frontend_table_selection_enabled() ) {
			wp_send_json_error( array( 'message' => __( 'Table selection is not available right now.', 'foodord' ) ) );
		}

		$deposit_required   = Foodord_Table_Reservations::calculate_deposit_amount( $preorder_date, $time, $party_size ) > 0;
		$hard_hold_required = $deposit_required || $preorder_available;

		if ( $hard_hold_required ) {
			$result = $table_id > 0
				? $this->reservations->create_hold_for_table( $date, $time, $party_size, $table_id, $customer_session, true )
				: $this->reservations->create_hold( $date, $time, $party_size, $customer_session, false, false, 0, true );
		} else {
			$result = $this->reservations->check_customer_availability( $date, $time, $party_size, $table_id );
		}
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		wp_send_json_success(
			array(
				'holdToken'         => $result['hold_token'],
				'holdRequired'      => $hard_hold_required,
				'expiresAtGmt'      => $result['expires_at_gmt'],
				'ttlSeconds'        => $result['ttl_seconds'],
				'tableId'           => $result['table_id'],
				'tableName'         => $result['table_name'],
				'tableCapacity'     => $result['capacity'],
				'preorderAvailable' => $preorder_available,
				'preorderRequired'  => $preorder_required,
			)
		);
	}

	/**
	 * Submit a reservation from an active hold or a fresh availability check.
	 */
	public function ajax_submit_reservation(): void {
		$this->verify_request();
		$this->verify_request_ip_allowed();
		$this->verify_customer_email_allowed( $this->get_post_text_field( 'email' ) );
		$privacy_settings = Foodord_Table_Reservations::get_privacy_settings();
		$privacy_consent  = '1' === $this->get_post_text_field( 'privacy_consent' );
		if ( $privacy_settings['enabled'] && ! $privacy_consent ) {
			wp_send_json_error( array( 'message' => __( 'Please accept the privacy statement to continue.', 'foodord' ) ), 400 );
		}
		$this->verify_recaptcha_request( self::RECAPTCHA_ACTION_SUBMIT );

		$reservation_data = array(
			'name'            => $this->get_post_text_field( 'name' ),
			'email'           => $this->get_post_text_field( 'email' ),
			'phone'           => $this->get_post_text_field( 'phone' ),
			'note'            => $this->get_post_textarea_field( 'note' ),
			'privacy_consent' => $privacy_consent,
		);
		$hold_token       = $this->get_post_text_field( 'hold_token' );

		if ( '' !== $hold_token ) {
			$result = $this->reservations->create_reservation_from_hold( $hold_token, $reservation_data );
		} else {
			$date               = $this->get_post_text_field( 'date' );
			$time               = $this->get_post_text_field( 'time' );
			$party_size         = absint( filter_input( INPUT_POST, 'party_size', FILTER_SANITIZE_NUMBER_INT ) );
			$table_id           = Foodord_Table_Reservations::is_frontend_table_selection_enabled()
				? absint( filter_input( INPUT_POST, 'table_id', FILTER_SANITIZE_NUMBER_INT ) )
				: 0;
			$slot_date          = Foodord_Table_Reservations::normalize_date_input( $date );
			$preorder_available = Foodord_Table_Reservation_Preorders::is_available_for_slot( $slot_date, $time, $party_size )
				&& ! empty( $this->preorders->get_catalog_products() );

			if ( Foodord_Table_Reservations::calculate_deposit_amount( $slot_date, $time, $party_size ) > 0 || $preorder_available ) {
				wp_send_json_error( array( 'message' => __( 'Please check availability again.', 'foodord' ) ), 409 );
			}

			$result = $this->reservations->create_reservation_without_hold( $date, $time, $party_size, $table_id, $reservation_data );
		}

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}

		$reservation = $this->reservations->get_reservation( (int) $result['reservation_id'] );
		if ( $reservation && ! empty( $result['deposit_required'] ) ) {
			if ( ! class_exists( 'Foodord_Table_Reservation_Deposits' ) ) {
				$this->reservations->mark_deposit_payment_failed( (int) $result['reservation_id'] );
				wp_send_json_error( array( 'message' => __( 'Online payment is not available right now.', 'foodord' ) ) );
			}

			$payment = Foodord_Table_Reservation_Deposits::create_payment_order( $reservation );
			if ( is_wp_error( $payment ) ) {
				$this->reservations->mark_deposit_payment_failed( (int) $result['reservation_id'] );
				wp_send_json_error( array( 'message' => $payment->get_error_message() ) );
			}

			wp_send_json_success(
				array(
					'redirectUrl'     => $payment['redirect_url'],
					'reservationCode' => $result['reservation_code'],
					'status'          => $result['status'],
				)
			);
		}

		if ( $reservation ) {
			if ( ! empty( $result['manage_token'] ) ) {
				$reservation->manage_token = (string) $result['manage_token'];
			}

			if ( 'confirmed' === (string) $result['status'] ) {
				$this->emails->send_confirmed_email( $reservation );
			} else {
				$this->emails->send_pending_email( $reservation );
			}

			$this->emails->send_admin_notification_email( $reservation );
		}

		$is_confirmed = 'confirmed' === (string) $result['status'];

		wp_send_json_success(
			array(
				'title'           => $is_confirmed ? __( 'Reservation confirmed', 'foodord' ) : __( 'Reservation received', 'foodord' ),
				'message'         => $is_confirmed ? $this->get_confirmed_success_message( (int) $result['party_size'] ) : __( 'Your reservation request has been received. We have sent the details to your email address and will notify you once it has been reviewed.', 'foodord' ),
				'reservationCode' => $result['reservation_code'],
				'status'          => $result['status'],
			)
		);
	}

	/**
	 * Get the confirmed reservation success message.
	 *
	 * @param int $party_size Reservation party size.
	 */
	private function get_confirmed_success_message( int $party_size ): string {
		$party_size = max( 1, absint( $party_size ) );

		return _n(
			'Your reservation is confirmed. We look forward to welcoming you.',
			'Your reservation is confirmed. We look forward to welcoming you and your guests.',
			$party_size,
			'foodord'
		);
	}

	/**
	 * Release active hold.
	 */
	public function ajax_release_hold(): void {
		$this->verify_request();

		$this->reservations->release_hold( $this->get_post_text_field( 'hold_token' ) );
		wp_send_json_success( array( 'message' => __( 'Table hold released.', 'foodord' ) ) );
	}

	/**
	 * Start or resume the pre-order lifecycle for an active table hold.
	 */
	public function ajax_start_preorder(): void {
		$this->verify_request();

		$result = $this->preorders->start_draft(
			$this->get_post_text_field( 'hold_token' ),
			$this->get_post_text_field( 'customer_session' ),
			$this->reservations
		);

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success( $this->format_preorder_lifecycle_response( $result ) );
	}

	/**
	 * Resume an active pre-order draft after a page refresh.
	 */
	public function ajax_resume_preorder(): void {
		$this->verify_request();

		$result = $this->preorders->resume_draft(
			$this->get_post_text_field( 'draft_token' ),
			$this->get_post_text_field( 'customer_session' )
		);

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 410 );
		}

		$draft = $this->preorders->get_authorized_draft(
			$this->get_post_text_field( 'draft_token' ),
			$this->get_post_text_field( 'customer_session' )
		);
		if ( Foodord_Table_Reservation_Preorders::is_payment_edit_draft( $draft ) ) {
			$order = function_exists( 'wc_get_order' ) ? wc_get_order( absint( $draft->order_id ) ) : false;
			if ( ! $order instanceof WC_Order || ! Foodord_Table_Reservation_Deposits::is_preorder_order_editable( $order ) ) {
				wp_send_json_error( array( 'message' => __( 'This pre-order can no longer be edited.', 'foodord' ) ), 410 );
			}
			Foodord_Table_Reservation_Deposits::begin_preorder_edit( $order );
		}

		wp_send_json_success( $this->format_preorder_lifecycle_response( $result ) );
	}

	/**
	 * Clear pre-order contents while preserving the remaining table hold.
	 */
	public function ajax_cancel_preorder(): void {
		$this->verify_request();

		$cancelled = $this->preorders->cancel_draft(
			$this->get_post_text_field( 'draft_token' ),
			$this->get_post_text_field( 'customer_session' )
		);

		if ( ! $cancelled ) {
			wp_send_json_error( array( 'message' => __( 'Unable to cancel the pre-order. Please try again.', 'foodord' ) ), 400 );
		}

		wp_send_json_success();
	}

	/**
	 * Cancel the complete pre-order flow and release its table hold.
	 */
	public function ajax_leave_preorder(): void {
		$this->verify_request();

		$draft = $this->preorders->get_authorized_draft(
			$this->get_post_text_field( 'draft_token' ),
			$this->get_post_text_field( 'customer_session' )
		);

		if ( ! $draft ) {
			wp_send_json_error( array( 'message' => __( 'Your pre-order session has expired. Please select a time again.', 'foodord' ) ), 410 );
		}
		if ( Foodord_Table_Reservation_Preorders::is_payment_edit_draft( $draft ) ) {
			$order = function_exists( 'wc_get_order' ) ? wc_get_order( absint( $draft->order_id ) ) : false;
			if ( ! $order instanceof WC_Order || ! Foodord_Table_Reservation_Deposits::cancel_editable_preorder_order( $order ) ) {
				wp_send_json_error( array( 'message' => __( 'Unable to cancel the reservation. Please try again.', 'foodord' ) ), 400 );
			}

			wp_send_json_success();
		}

		$this->reservations->release_hold( (string) $draft->hold_token );
		wp_send_json_success();
	}

	/**
	 * Add a product configuration to an active pre-order draft.
	 */
	public function ajax_add_preorder_item(): void {
		$this->verify_request();

		$result = $this->preorders->add_draft_item(
			$this->get_post_text_field( 'draft_token' ),
			$this->get_post_text_field( 'customer_session' ),
			$this->get_post_json_field( 'configuration' )
		);

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success( $result );
	}

	/**
	 * Update or remove a pre-order draft item.
	 */
	public function ajax_update_preorder_item(): void {
		$this->verify_request();
		$note = filter_has_var( INPUT_POST, 'note' ) ? $this->get_post_textarea_field( 'note' ) : null;

		$result = $this->preorders->update_draft_item(
			$this->get_post_text_field( 'draft_token' ),
			$this->get_post_text_field( 'customer_session' ),
			$this->get_post_text_field( 'item_key' ),
			(int) filter_input( INPUT_POST, 'quantity', FILTER_SANITIZE_NUMBER_INT ),
			$note
		);

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success( $result );
	}

	/**
	 * Remove every item from the active pre-order draft.
	 */
	public function ajax_clear_preorder_items(): void {
		$this->verify_request();

		$result = $this->preorders->clear_draft_items(
			$this->get_post_text_field( 'draft_token' ),
			$this->get_post_text_field( 'customer_session' )
		);

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		wp_send_json_success( $result );
	}

	/**
	 * Return the current pre-order draft summary.
	 */
	public function ajax_get_preorder_summary(): void {
		$this->verify_request();

		$result = $this->preorders->get_draft_summary(
			$this->get_post_text_field( 'draft_token' ),
			$this->get_post_text_field( 'customer_session' )
		);

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 410 );
		}

		wp_send_json_success( $result );
	}

	/**
	 * Convert a validated draft into one reservation payment order.
	 */
	public function ajax_checkout_preorder(): void {
		$this->verify_request();
		$this->verify_request_ip_allowed();
		$this->verify_customer_email_allowed( $this->get_post_text_field( 'email' ) );

		$privacy_settings = Foodord_Table_Reservations::get_privacy_settings();
		$privacy_consent  = '1' === $this->get_post_text_field( 'privacy_consent' );
		if ( $privacy_settings['enabled'] && ! $privacy_consent ) {
			wp_send_json_error( array( 'message' => __( 'Please accept the privacy statement to continue.', 'foodord' ) ), 400 );
		}
		$this->verify_recaptcha_request( self::RECAPTCHA_ACTION_SUBMIT );

		$draft_token      = $this->get_post_text_field( 'draft_token' );
		$customer_session = $this->get_post_text_field( 'customer_session' );
		$items            = $this->preorders->get_validated_draft_items( $draft_token, $customer_session );
		if ( is_wp_error( $items ) ) {
			wp_send_json_error( array( 'message' => $items->get_error_message() ), 400 );
		}
		if ( empty( $items ) ) {
			wp_send_json_error( array( 'message' => __( 'Add at least one product to continue.', 'foodord' ) ), 400 );
		}

		$draft = $this->preorders->get_authorized_draft( $draft_token, $customer_session );
		if ( ! $draft ) {
			wp_send_json_error( array( 'message' => __( 'Your pre-order session has expired. Please select a time again.', 'foodord' ) ), 410 );
		}
		if ( Foodord_Table_Reservation_Preorders::is_payment_edit_draft( $draft ) ) {
			$reservation = $this->reservations->get_reservation( absint( $draft->reservation_id ) );
			if ( ! $reservation || ! class_exists( 'Foodord_Table_Reservation_Deposits' ) ) {
				wp_send_json_error( array( 'message' => __( 'This pre-order can no longer be edited.', 'foodord' ) ), 410 );
			}

			$payment = Foodord_Table_Reservation_Deposits::update_preorder_payment_order( $reservation, $items );
			if ( is_wp_error( $payment ) || empty( $payment['redirect_url'] ) ) {
				$message = is_wp_error( $payment ) ? $payment->get_error_message() : __( 'Unable to return to payment. Please try again.', 'foodord' );
				wp_send_json_error( array( 'message' => $message ), 400 );
			}

			wp_send_json_success(
				array(
					'redirectUrl' => $payment['redirect_url'],
					'editUrl'     => $payment['edit_url'],
				)
			);
		}

		$result = $this->reservations->create_reservation_from_hold(
			(string) $draft->hold_token,
			array(
				'name'                    => $this->get_post_text_field( 'name' ),
				'email'                   => $this->get_post_text_field( 'email' ),
				'phone'                   => $this->get_post_text_field( 'phone' ),
				'note'                    => $this->get_post_textarea_field( 'note' ),
				'privacy_consent'         => $privacy_consent,
				'requires_payment'        => true,
				'preserve_preorder_draft' => true,
			)
		);
		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 400 );
		}

		$reservation = $this->reservations->get_reservation( (int) $result['reservation_id'] );
		if ( ! $reservation || ! class_exists( 'Foodord_Table_Reservation_Deposits' ) ) {
			$this->reservations->mark_deposit_payment_failed( (int) $result['reservation_id'] );
			wp_send_json_error( array( 'message' => __( 'Online payment is not available right now.', 'foodord' ) ), 500 );
		}

		$payment = Foodord_Table_Reservation_Deposits::create_preorder_payment_order( $reservation, $items );
		if ( is_wp_error( $payment ) ) {
			$this->reservations->mark_deposit_payment_failed( (int) $result['reservation_id'] );
			wp_send_json_error( array( 'message' => $payment->get_error_message() ), 500 );
		}

		$draft_attached = $this->preorders->attach_payment_order(
			$draft_token,
			$customer_session,
			(int) $result['reservation_id'],
			(int) $payment['order_id'],
			(string) $payment['expires_at_gmt']
		);
		if ( ! $draft_attached ) {
			$order = function_exists( 'wc_get_order' ) ? wc_get_order( (int) $payment['order_id'] ) : false;
			if ( $order instanceof WC_Order ) {
				$order->update_status( 'cancelled', __( 'Reservation pre-order editing session could not be created.', 'foodord' ) );
			}
			wp_send_json_error( array( 'message' => __( 'Unable to start the pre-order payment. Please try again.', 'foodord' ) ), 500 );
		}
		$payment_order = function_exists( 'wc_get_order' ) ? wc_get_order( (int) $payment['order_id'] ) : false;
		$edit_url      = $payment_order instanceof WC_Order ? Foodord_Table_Reservation_Deposits::get_preorder_edit_url( $payment_order ) : '';
		if ( '' === $edit_url ) {
			wp_send_json_error( array( 'message' => __( 'Unable to create the pre-order return link.', 'foodord' ) ), 500 );
		}

		wp_send_json_success(
			array(
				'redirectUrl'     => $payment['redirect_url'],
				'editUrl'         => $edit_url,
				'reservationCode' => $result['reservation_code'],
			)
		);
	}

	/**
	 * Format lifecycle data without exposing internal database fields.
	 *
	 * @param array<string,mixed> $result Pre-order lifecycle result.
	 * @return array<string,mixed>
	 */
	private function format_preorder_lifecycle_response( array $result ): array {
		return array(
			'draftToken'     => (string) $result['draft_token'],
			'holdToken'      => (string) $result['hold_token'],
			'expiresAtGmt'   => (string) $result['expires_at_gmt'],
			'ttlSeconds'     => (int) $result['ttl_seconds'],
			'holdExtended'   => ! empty( $result['hold_extended'] ),
			'preorderStatus' => (string) $result['preorder_status'],
			'paymentEdit'    => 'payment_edit' === (string) $result['preorder_status'],
		);
	}

	/**
	 * Enqueue assets for standalone reservation management requests.
	 */
	public function maybe_enqueue_manage_assets(): void {
		if ( $this->is_manage_request() ) {
			$this->enqueue_assets();
		}
	}

	/**
	 * Render the standalone customer reservation management screen.
	 */
	public function maybe_render_manage_page(): void {
		if ( ! $this->is_manage_request() ) {
			return;
		}

		if ( $this->current_page_has_reservations_renderer() ) {
			return;
		}

		$this->maybe_redirect_manage_request_to_reservations_page();

		$this->is_manage_request = true;
		status_header( 200 );
		get_header();
		$this->render_manage_page();
		get_footer();
		exit;
	}

	/**
	 * Handle customer reservation updates.
	 */
	public function handle_manage_reservation_update(): void {
		check_admin_referer( self::MANAGE_NONCE_ACTION );

		$reservation_id = absint( filter_input( INPUT_POST, 'reservation_id', FILTER_SANITIZE_NUMBER_INT ) );
		$token          = $this->get_post_text_field( 'token' );
		$result         = $this->reservations->update_customer_managed_reservation(
			$reservation_id,
			$token,
			array(
				'date'       => $this->get_post_text_field( 'date' ),
				'time'       => $this->get_post_text_field( 'time' ),
				'party_size' => absint( filter_input( INPUT_POST, 'party_size', FILTER_SANITIZE_NUMBER_INT ) ),
				'name'       => $this->get_post_text_field( 'name' ),
				'email'      => $this->get_post_text_field( 'email' ),
				'phone'      => $this->get_post_text_field( 'phone' ),
				'note'       => $this->get_post_textarea_field( 'note' ),
			)
		);

		if ( is_wp_error( $result ) ) {
			$this->redirect_to_manage_page( $reservation_id, $token, 'error', $result->get_error_message() );
		}

		if ( false === $result ) {
			$this->redirect_to_manage_page( $reservation_id, $token, '' );
		}

		$manage_token = sanitize_text_field( (string) ( $result['manage_token'] ?? $token ) );
		$reservation  = $this->reservations->get_reservation( $reservation_id );
		if ( $reservation ) {
			$reservation->manage_token = $manage_token;
			if ( ! empty( $result['slot_changed'] ) && 'confirmed' === Foodord_Table_Reservations::sanitize_status( (string) $reservation->status ) ) {
				$this->emails->send_confirmed_email( $reservation );
			} elseif ( ! empty( $result['slot_changed'] ) ) {
				$this->emails->send_pending_email( $reservation );
			} else {
				$this->emails->send_updated_email( $reservation );
			}

			$this->emails->send_admin_updated_email( $reservation, $result['changed_fields'] ?? array() );

			if ( ! empty( $result['email_changed'] ) ) {
				$this->emails->send_previous_email_change_notice(
					$reservation,
					(string) ( $result['old_email'] ?? '' )
				);
			}
		}

		$this->redirect_to_manage_page( $reservation_id, $manage_token, 'updated' );
	}

	/**
	 * Handle customer reservation cancellation.
	 */
	public function handle_manage_reservation_cancel(): void {
		check_admin_referer( self::MANAGE_NONCE_ACTION );

		$reservation_id = absint( filter_input( INPUT_POST, 'reservation_id', FILTER_SANITIZE_NUMBER_INT ) );
		$token          = $this->get_post_text_field( 'token' );
		$result         = $this->reservations->cancel_customer_managed_reservation( $reservation_id, $token );

		if ( is_wp_error( $result ) ) {
			$this->redirect_to_manage_page( $reservation_id, $token, 'error', $result->get_error_message() );
		}

		$reservation = $this->reservations->get_reservation( $reservation_id );
		if ( $reservation ) {
			$this->emails->send_cancelled_email( $reservation );
			$this->emails->send_admin_cancelled_email( $reservation );
		}

		$this->redirect_to_manage_page( $reservation_id, $token, 'cancelled' );
	}

	/**
	 * Render the customer reservation management page.
	 */
	private function render_manage_page(): void {
		$reservation_id = absint( filter_input( INPUT_GET, 'foodord_reservation_manage', FILTER_SANITIZE_NUMBER_INT ) );
		$token          = $this->get_query_text_field( 'foodord_reservation_token' );
		$reservation    = $this->reservations->get_reservation_by_manage_token( $reservation_id, $token );
		$banner_url     = $this->get_banner_image_url();
		$root_class     = 'foodord-table-reservations foodord-table-reservations--manage ' . ( '' !== $banner_url ? 'foodord-table-reservations--with-banner' : 'foodord-table-reservations--no-banner' );
		$manage_message = sanitize_key( $this->get_query_text_field( 'foodord_reservation_message' ) );
		$is_updated     = ! is_wp_error( $reservation ) && 'updated' === $manage_message;
		?>
		<div
			class="<?php echo esc_attr( $root_class ); ?>"
			data-foodord-reservation-manage
			<?php if ( $is_updated ) : ?>
				data-foodord-reservation-step="done"
			<?php endif; ?>
		>
			<div class="foodord-table-reservations__layout">
				<?php if ( '' !== $banner_url ) : ?>
					<div class="foodord-table-reservations__banner" aria-hidden="true">
						<img src="<?php echo esc_url( $banner_url ); ?>" alt="" loading="lazy" />
					</div>
				<?php endif; ?>

				<div class="foodord-table-reservations__content">
					<div class="foodord-table-reservations__form foodord-table-reservations__manage-card">
						<?php if ( is_wp_error( $reservation ) ) : ?>
							<?php $this->render_manage_notice( $reservation->get_error_message(), 'error' ); ?>
						<?php elseif ( $is_updated ) : ?>
							<?php $this->render_reservation_timeline( false ); ?>
							<?php $this->render_manage_update_success( $reservation ); ?>
						<?php else : ?>
							<h2 class="foodord-table-reservations__form-title"><?php esc_html_e( 'Manage reservation', 'foodord' ); ?></h2>
							<p class="foodord-table-reservations__form-description"><?php esc_html_e( 'Update your details or cancel this reservation before it starts.', 'foodord' ); ?></p>
							<?php $this->render_manage_status_message( $manage_message ); ?>
							<?php if ( 'cancelled' === Foodord_Table_Reservations::sanitize_status( (string) $reservation->status ) ) : ?>
								<?php $this->render_manage_notice( __( 'Your reservation has been cancelled.', 'foodord' ), 'success' ); ?>
							<?php elseif ( ! $this->reservations->can_customer_manage_reservation( $reservation ) ) : ?>
								<?php $this->render_manage_notice( __( 'This reservation cannot be changed right now. Please contact the restaurant.', 'foodord' ), 'error' ); ?>
							<?php else : ?>
								<?php $this->render_manage_form( $reservation, $token ); ?>
							<?php endif; ?>
						<?php endif; ?>
					</div>
				</div>
			</div>
		</div>
		<?php
	}

	/**
	 * Render the success screen after a customer updates a reservation.
	 *
	 * @param object $reservation Reservation row.
	 */
	private function render_manage_update_success( $reservation ): void {
		$reservation_code = isset( $reservation->reservation_code ) ? (string) $reservation->reservation_code : '';
		?>
		<div class="foodord-table-reservations__success" role="status" tabindex="-1" data-foodord-reservation-manage-success>
			<span class="foodord-table-reservations__success-icon" aria-hidden="true"></span>
			<h3 class="foodord-table-reservations__success-title"><?php esc_html_e( 'Reservation updated', 'foodord' ); ?></h3>
			<p class="foodord-table-reservations__success-text"><?php esc_html_e( 'Your reservation has been updated. We have sent the latest details to your email address.', 'foodord' ); ?></p>
			<?php if ( '' !== $reservation_code ) : ?>
				<p class="foodord-table-reservations__success-code-row">
					<span><?php esc_html_e( 'Reservation code', 'foodord' ); ?></span>
					<strong><?php echo esc_html( $reservation_code ); ?></strong>
				</p>
			<?php endif; ?>
			<form class="foodord-table-reservations__success-home-form" method="get" action="<?php echo esc_url( home_url( '/' ) ); ?>">
				<button class="foodord-table-reservations__button foodord-table-reservations__button--home" type="submit">
					<svg class="foodord-table-reservations__button-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
						<path d="M3 10.5 12 3l9 7.5"></path>
						<path d="M5 10v10h14V10"></path>
						<path d="M9 20v-6h6v6"></path>
					</svg>
					<?php esc_html_e( 'Back to home', 'foodord' ); ?>
				</button>
			</form>
		</div>
		<?php
	}

	/**
	 * Render the management edit and cancellation forms.
	 *
	 * @param object $reservation Reservation row.
	 * @param string $token       Raw management token.
	 */
	private function render_manage_form( $reservation, string $token ): void {
		$date               = isset( $reservation->reservation_date ) ? (string) $reservation->reservation_date : '';
		$time               = isset( $reservation->reservation_time ) ? substr( (string) $reservation->reservation_time, 0, 5 ) : '';
		$party_size         = isset( $reservation->party_size ) ? max( 1, absint( $reservation->party_size ) ) : 1;
		$slot_fields_locked = 'paid' === sanitize_key( (string) ( $reservation->payment_status ?? '' ) );
		$times              = $slot_fields_locked ? array( $time ) : $this->reservations->get_time_options_for_date( $date, $party_size, (int) $reservation->id );
		$can_cancel         = $this->reservations->can_customer_cancel_reservation( $reservation );
		if ( '' !== $time && ! in_array( $time, $times, true ) ) {
			array_unshift( $times, $time );
			$times = array_values( array_unique( $times ) );
		}
		$time_labels = $this->get_time_slot_labels( $times );
		?>
		<form class="foodord-table-reservations__manage-form" method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" data-foodord-reservation-manage-form>
			<?php wp_nonce_field( self::MANAGE_NONCE_ACTION ); ?>
			<input class="foodord-table-reservations__hidden-field" type="hidden" name="action" value="foodord_manage_reservation_update" />
			<input class="foodord-table-reservations__hidden-field" type="hidden" name="reservation_id" value="<?php echo esc_attr( (int) $reservation->id ); ?>" />
			<input class="foodord-table-reservations__hidden-field" type="hidden" name="token" value="<?php echo esc_attr( $token ); ?>" />
			<div class="foodord-table-reservations__grid">
				<label class="foodord-table-reservations__field foodord-table-reservations__field--date foodord-table-reservations__field--required" for="foodord-reservation-manage-date">
					<span><?php esc_html_e( 'Date', 'foodord' ); ?></span>
					<input id="foodord-reservation-manage-date" class="foodord-table-reservations__control" type="text" inputmode="numeric" autocomplete="off" name="date" value="<?php echo esc_attr( $this->format_manage_date_value( $date ) ); ?>" placeholder="<?php echo esc_attr( $this->get_date_placeholder() ); ?>" required <?php disabled( $slot_fields_locked ); ?> />
				</label>
				<label class="foodord-table-reservations__field foodord-table-reservations__field--time foodord-table-reservations__field--required" for="foodord-reservation-manage-time">
					<span><?php esc_html_e( 'Time', 'foodord' ); ?></span>
					<select id="foodord-reservation-manage-time" class="foodord-table-reservations__control" name="time" required <?php disabled( $slot_fields_locked ); ?>>
						<?php foreach ( $times as $option_time ) : ?>
							<option value="<?php echo esc_attr( $option_time ); ?>" <?php selected( $time, $option_time ); ?>><?php echo esc_html( $time_labels[ $option_time ] ?? $option_time ); ?></option>
						<?php endforeach; ?>
					</select>
				</label>
				<label class="foodord-table-reservations__field foodord-table-reservations__field--required" for="foodord-reservation-manage-party-size">
					<span><?php esc_html_e( 'Guests', 'foodord' ); ?></span>
					<input id="foodord-reservation-manage-party-size" class="foodord-table-reservations__control" type="number" name="party_size" min="1" step="1" value="<?php echo esc_attr( $party_size ); ?>" required <?php disabled( $slot_fields_locked ); ?> />
				</label>
			</div>
			<?php if ( $slot_fields_locked ) : ?>
				<input class="foodord-table-reservations__hidden-field" type="hidden" name="date" value="<?php echo esc_attr( $date ); ?>" />
				<input class="foodord-table-reservations__hidden-field" type="hidden" name="time" value="<?php echo esc_attr( $time ); ?>" />
				<input class="foodord-table-reservations__hidden-field" type="hidden" name="party_size" value="<?php echo esc_attr( $party_size ); ?>" />
			<?php endif; ?>
			<div class="foodord-table-reservations__grid foodord-table-reservations__manage-customer-grid">
				<label class="foodord-table-reservations__field foodord-table-reservations__field--required" for="foodord-reservation-manage-name">
					<span><?php esc_html_e( 'Name', 'foodord' ); ?></span>
					<input id="foodord-reservation-manage-name" class="foodord-table-reservations__control" type="text" name="name" autocomplete="name" value="<?php echo esc_attr( (string) $reservation->customer_name ); ?>" required />
				</label>
				<label class="foodord-table-reservations__field foodord-table-reservations__field--required" for="foodord-reservation-manage-email">
					<span><?php esc_html_e( 'Email', 'foodord' ); ?></span>
					<input id="foodord-reservation-manage-email" class="foodord-table-reservations__control" type="email" name="email" autocomplete="email" value="<?php echo esc_attr( (string) $reservation->customer_email ); ?>" required />
				</label>
				<label class="foodord-table-reservations__field foodord-table-reservations__field--required" for="foodord-reservation-manage-phone">
					<span><?php esc_html_e( 'Phone', 'foodord' ); ?></span>
					<input id="foodord-reservation-manage-phone" class="foodord-table-reservations__control" type="tel" name="phone" autocomplete="tel" value="<?php echo esc_attr( (string) $reservation->customer_phone ); ?>" required />
				</label>
			</div>
			<label class="foodord-table-reservations__field foodord-table-reservations__field--note" for="foodord-reservation-manage-note">
				<span><?php esc_html_e( 'Additional Request', 'foodord' ); ?></span>
				<textarea id="foodord-reservation-manage-note" class="foodord-table-reservations__control" name="note" rows="3"><?php echo esc_textarea( (string) $reservation->customer_note ); ?></textarea>
			</label>
			<button class="foodord-table-reservations__button foodord-table-reservations__button--primary" type="submit">
				<?php esc_html_e( 'Update changes', 'foodord' ); ?>
			</button>
		</form>
		<?php if ( $can_cancel ) : ?>
			<div class="foodord-table-reservations__manage-cancel-form">
				<button class="foodord-table-reservations__manage-cancel-trigger" type="button" data-foodord-reservation-cancel-open>
					<?php esc_html_e( 'Cancel reservation', 'foodord' ); ?>
				</button>
			</div>
			<div class="foodord-table-reservations__confirm" data-foodord-reservation-cancel-modal hidden>
				<div class="foodord-table-reservations__confirm-backdrop" data-foodord-reservation-cancel-close></div>
				<div class="foodord-table-reservations__confirm-dialog" role="dialog" aria-modal="true" aria-labelledby="foodord-reservation-cancel-title">
					<h3 id="foodord-reservation-cancel-title" class="foodord-table-reservations__confirm-title"><?php esc_html_e( 'Cancel reservation?', 'foodord' ); ?></h3>
					<p class="foodord-table-reservations__confirm-text"><?php esc_html_e( 'Are you sure you want to cancel this reservation?', 'foodord' ); ?></p>
					<div class="foodord-table-reservations__confirm-actions">
						<button class="foodord-table-reservations__button foodord-table-reservations__confirm-button foodord-table-reservations__confirm-button--secondary" type="button" data-foodord-reservation-cancel-close>
							<?php esc_html_e( 'Keep reservation', 'foodord' ); ?>
						</button>
						<form class="foodord-table-reservations__confirm-form" method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" data-foodord-reservation-cancel-form>
							<?php wp_nonce_field( self::MANAGE_NONCE_ACTION ); ?>
							<input class="foodord-table-reservations__hidden-field" type="hidden" name="action" value="foodord_manage_reservation_cancel" />
							<input class="foodord-table-reservations__hidden-field" type="hidden" name="reservation_id" value="<?php echo esc_attr( (int) $reservation->id ); ?>" />
							<input class="foodord-table-reservations__hidden-field" type="hidden" name="token" value="<?php echo esc_attr( $token ); ?>" />
							<button class="foodord-table-reservations__button foodord-table-reservations__confirm-button foodord-table-reservations__confirm-button--danger" type="submit" data-foodord-reservation-cancel-confirm>
								<?php esc_html_e( 'Cancel reservation', 'foodord' ); ?>
							</button>
						</form>
					</div>
				</div>
			</div>
		<?php endif; ?>
		<?php
	}

	/**
	 * Render a management screen status message.
	 *
	 * @param string $message Message key.
	 */
	private function render_manage_status_message( string $message ): void {
		if ( 'error' === $message ) {
			$detail = $this->get_query_text_field( 'foodord_reservation_error' );
			$this->render_manage_notice( '' !== $detail ? $detail : __( 'Unable to update the reservation.', 'foodord' ), 'error' );
		}
	}

	/**
	 * Render a management notice.
	 *
	 * @param string $message Notice message.
	 * @param string $type    Notice type.
	 */
	private function render_manage_notice( string $message, string $type ): void {
		$type = 'success' === $type ? 'success' : 'error';
		?>
		<div class="foodord-table-reservations__manage-notice is-<?php echo esc_attr( $type ); ?>" role="status">
			<?php echo esc_html( $message ); ?>
		</div>
		<?php
	}

	/**
	 * Redirect back to the customer management page.
	 *
	 * @param int    $reservation_id Reservation ID.
	 * @param string $token          Raw management token.
	 * @param string $message        Message key.
	 * @param string $error          Optional error message.
	 */
	private function redirect_to_manage_page( int $reservation_id, string $token, string $message, string $error = '' ): void {
		$url = Foodord_Table_Reservations::get_manage_reservation_url(
			(object) array( 'id' => $reservation_id ),
			$token
		);

		if ( '' === $url ) {
			$url = home_url( '/' );
		}

		$args = array();
		if ( '' !== $message ) {
			$args['foodord_reservation_message'] = sanitize_key( $message );
		}

		if ( '' !== $error ) {
			$args['foodord_reservation_error'] = sanitize_text_field( $error );
		}

		wp_safe_redirect( empty( $args ) ? $url : add_query_arg( $args, $url ) );
		exit;
	}

	/**
	 * Check whether this is a customer reservation management request.
	 */
	private function is_manage_request(): bool {
		if ( $this->is_manage_request ) {
			return true;
		}

		return absint( filter_input( INPUT_GET, 'foodord_reservation_manage', FILTER_SANITIZE_NUMBER_INT ) ) > 0
			&& '' !== $this->get_query_text_field( 'foodord_reservation_token' );
	}

	/**
	 * Check whether the current page renders the reservation shortcode or block.
	 */
	private function current_page_has_reservations_renderer(): bool {
		if ( ! is_singular() ) {
			return false;
		}

		$post = get_post();
		if ( ! $post instanceof WP_Post ) {
			return false;
		}

		$content = (string) $post->post_content;
		if ( has_shortcode( $content, 'foodord_reservations' ) ) {
			return true;
		}

		return function_exists( 'has_block' ) && has_block( 'foodord/reservations', $post );
	}

	/**
	 * Redirect legacy management links to the actual reservations page.
	 */
	private function maybe_redirect_manage_request_to_reservations_page(): void {
		$reservation_id = absint( filter_input( INPUT_GET, 'foodord_reservation_manage', FILTER_SANITIZE_NUMBER_INT ) );
		$token          = $this->get_query_text_field( 'foodord_reservation_token' );
		$url            = Foodord_Table_Reservations::get_manage_reservation_url(
			(object) array( 'id' => $reservation_id ),
			$token
		);

		if ( '' === $url ) {
			return;
		}

		global $wp;

		$current_request = isset( $wp->request ) ? (string) $wp->request : '';
		$target_path     = $this->normalize_request_path( (string) wp_parse_url( $url, PHP_URL_PATH ) );
		$current_path    = $this->normalize_request_path( (string) wp_parse_url( home_url( $current_request ), PHP_URL_PATH ) );

		if ( $target_path === $current_path ) {
			return;
		}

		wp_safe_redirect( $url );
		exit;
	}

	/**
	 * Normalize request paths before canonical URL comparisons.
	 *
	 * @param string $path URL path.
	 */
	private function normalize_request_path( string $path ): string {
		$path = '/' . ltrim( $path, '/' );
		$path = untrailingslashit( $path );

		return '' === $path ? '/' : $path;
	}

	/**
	 * Format a reservation date for the current WordPress date format.
	 *
	 * @param string $date Date in Y-m-d format.
	 */
	private function format_manage_date_value( string $date ): string {
		$timestamp = strtotime( $date . ' 00:00:00' );
		if ( false === $timestamp ) {
			return $date;
		}

		return date_i18n( get_option( 'date_format' ), $timestamp );
	}

	/**
	 * Enqueue frontend assets.
	 */
	private function enqueue_assets(): void {
		$recaptcha_settings = Foodord_Table_Reservations::get_recaptcha_settings();
		$recaptcha_enabled  = Foodord_Table_Reservations::is_recaptcha_enabled();
		$dependencies       = array( 'jquery', 'jquery-ui-datepicker' );

		if ( $recaptcha_enabled ) {
			wp_enqueue_script(
				'foodord-reservations-recaptcha',
				add_query_arg( 'render', $recaptcha_settings['site_key'], 'https://www.google.com/recaptcha/api.js' ),
				array(),
				FOODORD_VERSION,
				true
			);

			$dependencies[] = 'foodord-reservations-recaptcha';
		}

		wp_enqueue_style(
			'foodord-table-reservations',
			FOODORD_URL . 'public/css/foodord-table-reservations.css',
			array(),
			FOODORD_VERSION,
			'all'
		);

		wp_enqueue_script(
			'foodord-table-reservations',
			FOODORD_URL . 'public/js/foodord-table-reservations.js',
			$dependencies,
			FOODORD_VERSION,
			true
		);

		wp_localize_script(
			'foodord-table-reservations',
			'FoodordTableReservations',
			array(
				'ajaxUrl'         => admin_url( 'admin-ajax.php' ),
				'dateFormat'      => $this->get_jquery_date_format(),
				'dateMinOffset'   => Foodord_Table_Reservations::get_datepicker_minimum_offset(),
				'dateMaxOffset'   => Foodord_Table_Reservations::get_datepicker_maximum_offset(),
				'dateDisabled'    => $this->get_datepicker_disabled_rules(),
				'homeUrl'         => home_url( '/' ),
				'reservationsUrl' => Foodord_Table_Reservations::get_public_reservations_page_url(),
				'nonce'           => wp_create_nonce( self::NONCE_ACTION ),
				'noteMaxLength'   => 160,
				'recaptcha'       => array(
					'enabled' => $recaptcha_enabled,
					'siteKey' => $recaptcha_enabled ? $recaptcha_settings['site_key'] : '',
					'actions' => array(
						'hold'   => self::RECAPTCHA_ACTION_HOLD,
						'submit' => self::RECAPTCHA_ACTION_SUBMIT,
					),
				),
				'i18n'            => array(
					'enterEmail'          => __( 'Please enter a valid email address.', 'foodord' ),
					'enterGuests'         => __( 'Please enter the number of guests.', 'foodord' ),
					'enterName'           => __( 'Please enter your name.', 'foodord' ),
					'enterPhone'          => __( 'Please enter your phone number.', 'foodord' ),
					'acceptPrivacy'       => __( 'Please accept the privacy statement to continue.', 'foodord' ),
					'genericError'        => __( 'Something went wrong. Please try again.', 'foodord' ),
					'holdExpired'         => __( 'Your table hold has expired. Please select a time again.', 'foodord' ),
					'preorderEditExpired' => __( 'This pre-order can no longer be edited.', 'foodord' ),
					'noTimesAvailable'    => __( 'Reservations are not available on this date. Please choose another date.', 'foodord' ),
					'selectDate'          => __( 'Please select a date.', 'foodord' ),
					'selectDateAndTime'   => __( 'Please select a date and time.', 'foodord' ),
					'selectTime'          => __( 'Please select a time.', 'foodord' ),
					'timePlaceholder'     => __( 'Time', 'foodord' ),
					'securityFailed'      => __( 'Security verification failed. Please try again.', 'foodord' ),
					'decreaseQuantity'    => __( 'Decrease quantity', 'foodord' ),
					'increaseQuantity'    => __( 'Increase quantity', 'foodord' ),
					'remove'              => __( 'Remove', 'foodord' ),
					'addNote'             => __( 'Add note', 'foodord' ),
					'editNote'            => __( 'Edit note', 'foodord' ),
					'add'                 => __( 'Add', 'foodord' ),
					'cancel'              => __( 'Cancel', 'foodord' ),
					'delete'              => __( 'Delete', 'foodord' ),
					'adding'              => __( 'Adding…', 'foodord' ),
					'note'                => __( 'Note', 'foodord' ),
					'notePlaceholder'     => __( 'Add a note (max {max} characters)', 'foodord' ),
					'noteError'           => __( 'Could not update the note. Please try again.', 'foodord' ),
				),
			)
		);
	}

	/**
	 * Verify AJAX nonce.
	 */
	private function verify_request(): void {
		$nonce = $this->get_post_text_field( 'nonce' );
		if ( ! wp_verify_nonce( $nonce, self::NONCE_ACTION ) ) {
			wp_send_json_error( array( 'message' => __( 'Invalid request.', 'foodord' ) ), 403 );
		}
	}

	/**
	 * Verify reCAPTCHA for protected reservation actions.
	 *
	 * @param string $action Expected reCAPTCHA action.
	 */
	private function verify_recaptcha_request( string $action ): void {
		$result = Foodord_Table_Reservations::verify_recaptcha_token(
			$this->get_post_text_field( 'recaptcha_token' ),
			$action
		);

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ), 403 );
		}
	}

	/**
	 * Stop reservation requests from blocked IP addresses.
	 */
	private function verify_request_ip_allowed(): void {
		$ip_address = Foodord_Table_Reservations::get_request_ip_address();
		if ( '' !== $ip_address && Foodord_Table_Reservations::is_reservation_ip_blocked( $ip_address ) ) {
			wp_send_json_error( array( 'message' => $this->get_blocked_reservation_message() ), 403 );
		}
	}

	/**
	 * Stop reservation submissions from blocked email addresses or domains.
	 *
	 * @param string $email Email address.
	 */
	private function verify_customer_email_allowed( string $email ): void {
		if ( Foodord_Table_Reservations::is_reservation_email_blocked( $email ) ) {
			wp_send_json_error( array( 'message' => $this->get_blocked_reservation_message() ), 403 );
		}
	}

	/**
	 * Get the public rejection message for blocked reservation attempts.
	 */
	private function get_blocked_reservation_message(): string {
		return __( 'We could not accept this reservation. Please contact the restaurant.', 'foodord' );
	}

	/**
	 * Read sanitized text field from POST.
	 *
	 * @param string $key Field key.
	 * @return string
	 */
	private function get_post_text_field( string $key ): string {
		$value = filter_input( INPUT_POST, $key, FILTER_SANITIZE_FULL_SPECIAL_CHARS );

		return is_string( $value ) ? sanitize_text_field( wp_unslash( wp_specialchars_decode( $value, ENT_QUOTES ) ) ) : '';
	}

	/**
	 * Read sanitized textarea field from POST.
	 *
	 * @param string $key Field key.
	 * @return string
	 */
	private function get_post_textarea_field( string $key ): string {
		$value = filter_input( INPUT_POST, $key, FILTER_SANITIZE_FULL_SPECIAL_CHARS );

		return is_string( $value ) ? sanitize_textarea_field( wp_unslash( wp_specialchars_decode( $value, ENT_QUOTES ) ) ) : '';
	}

	/**
	 * Read a JSON object from POST.
	 *
	 * @param string $key Field key.
	 * @return array<string,mixed>
	 */
	private function get_post_json_field( string $key ): array {
		$value = filter_input(
			INPUT_POST,
			$key,
			FILTER_CALLBACK,
			array( 'options' => 'sanitize_textarea_field' )
		);
		$value = is_string( $value ) ? $value : '';
		$data  = '' !== $value ? json_decode( $value, true ) : array();

		return is_array( $data ) ? $data : array();
	}

	/**
	 * Read sanitized text field from GET.
	 *
	 * @param string $key Field key.
	 * @return string
	 */
	private function get_query_text_field( string $key ): string {
		$value = filter_input( INPUT_GET, $key, FILTER_SANITIZE_FULL_SPECIAL_CHARS );

		return is_string( $value ) ? sanitize_text_field( wp_unslash( $value ) ) : '';
	}
}
