<?php
/**
 * WooCommerce deposit payments for table reservations.
 *
 * @package FoodOrd
 */

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

/**
 * Class Foodord_Table_Reservation_Deposits
 */
class Foodord_Table_Reservation_Deposits {
	private const RESERVATION_PAYMENT_EMAIL_IDS          = array(
		'new_order',
		'cancelled_order',
		'failed_order',
		'customer_on_hold_order',
		'customer_processing_order',
		'customer_completed_order',
		'customer_failed_order',
		'customer_cancelled_order',
	);
	private const RESERVATION_PAYMENT_EMAILS_TO_SUPPRESS = array(
		'new_order',
		'customer_on_hold_order',
		'customer_processing_order',
		'customer_completed_order',
	);

	private const META_RESERVATION_ID    = '_foodord_reservation_id';
	private const META_RESERVATION_CODE  = '_foodord_reservation_code';
	private const META_DEPOSIT_ORDER     = '_foodord_reservation_deposit_order';
	private const META_PREORDER_ORDER    = '_foodord_reservation_preorder';
	private const META_PREORDER_EDITING  = '_foodord_reservation_preorder_editing';
	private const META_EXPIRES_AT_GMT    = '_foodord_reservation_deposit_expires_at_gmt';
	private const META_REVIEW_REQUIRED   = '_foodord_reservation_payment_review_required';
	private const PAYMENT_WINDOW_SECONDS = 300;
	private const OFFLINE_GATEWAY_IDS    = array( 'bacs', 'cheque', 'cod' );

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

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

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

	/**
	 * Bootstrap WooCommerce deposit hooks.
	 */
	public function init(): void {
		foreach ( self::RESERVATION_PAYMENT_EMAIL_IDS as $email_id ) {
			add_filter( 'woocommerce_email_subject_' . $email_id, array( $this, 'filter_reservation_payment_email_subject' ), 20, 3 );
			add_filter( 'woocommerce_email_heading_' . $email_id, array( $this, 'filter_reservation_payment_email_heading' ), 20, 3 );
		}
		foreach ( self::RESERVATION_PAYMENT_EMAILS_TO_SUPPRESS as $email_id ) {
			add_filter( 'woocommerce_email_enabled_' . $email_id, array( $this, 'filter_reservation_payment_email_enabled' ), 20, 3 );
		}

		add_action( 'woocommerce_email_before_order_table', array( $this, 'render_reservation_payment_email_details' ), 7, 4 );
		add_filter( 'foodord_processing_order_email_track_url', array( $this, 'filter_reservation_payment_track_url' ), 20, 2 );
		add_filter( 'woocommerce_email_styles', array( $this, 'append_reservation_payment_email_styles' ), 20, 2 );
		add_action( 'init', array( $this, 'cleanup_expired_deposits' ) );
		add_action( 'template_redirect', array( $this, 'clear_stale_cancelled_order_notice' ), 5 );
		add_action( 'template_redirect', array( $this, 'maybe_redirect_paid_deposit_return' ), 20 );
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_payment_assets' ) );
		add_action( 'before_woocommerce_pay_form', array( $this, 'render_payment_intro' ), 5, 3 );
		add_action( 'woocommerce_pay_order_after_submit', array( $this, 'render_cancel_payment_control' ) );
		add_filter( 'body_class', array( $this, 'add_payment_body_class' ) );
		add_filter( 'woocommerce_endpoint_order-pay_title', array( $this, 'filter_order_pay_title' ), 20, 3 );
		add_filter( 'woocommerce_pay_order_button_text', array( $this, 'filter_pay_button_text' ), 20, 1 );
		add_filter( 'woocommerce_order_cancelled_notice', array( $this, 'filter_cancelled_order_notice' ), 20, 1 );
		add_filter( 'woocommerce_available_payment_gateways', array( $this, 'filter_payment_gateways' ), 20, 1 );
		add_filter( 'woocommerce_valid_order_statuses_for_payment', array( $this, 'filter_valid_order_statuses_for_payment' ), 20, 2 );
		add_filter( 'woocommerce_get_order_item_totals', array( $this, 'filter_order_item_totals' ), 20, 3 );
		add_action( 'woocommerce_payment_complete', array( $this, 'handle_paid_order' ), 20, 1 );
		add_action( 'woocommerce_order_status_processing', array( $this, 'handle_paid_order' ), 20, 1 );
		add_action( 'woocommerce_order_status_completed', array( $this, 'handle_paid_order' ), 20, 1 );
		add_action( 'woocommerce_order_status_failed', array( $this, 'handle_failed_order' ), 20, 1 );
		add_action( 'woocommerce_order_status_cancelled', array( $this, 'handle_failed_order' ), 20, 1 );
	}

	/**
	 * Keep reservation payment details in the FoodOrd reservation emails only.
	 *
	 * @param bool  $enabled Whether the WooCommerce email is enabled.
	 * @param mixed $order   WooCommerce order object.
	 * @param mixed $email   WooCommerce email instance.
	 */
	public function filter_reservation_payment_email_enabled( $enabled, $order, $email ): bool {
		unset( $email );

		if ( $order instanceof WC_Order && self::is_deposit_order_object( $order ) ) {
			return false;
		}

		return (bool) $enabled;
	}

	/**
	 * Identify reservation payments in WooCommerce email subjects.
	 *
	 * @param string $subject Default email subject.
	 * @param mixed  $order   Email order object.
	 * @param mixed  $email   WooCommerce email object.
	 */
	public function filter_reservation_payment_email_subject( $subject, $order, $email ): string {
		unset( $email );

		if ( ! $order instanceof WC_Order || ! self::is_deposit_order_object( $order ) ) {
			return (string) $subject;
		}

		$reservation_code = trim( (string) $order->get_meta( self::META_RESERVATION_CODE, true ) );
		$email_label      = $this->get_reservation_payment_email_label( $order );

		return '' !== $reservation_code
			? $email_label . ' - ' . $reservation_code
			: $email_label;
	}

	/**
	 * Identify reservation payments in WooCommerce email headings.
	 *
	 * @param string $heading Default email heading.
	 * @param mixed  $order   Email order object.
	 * @param mixed  $email   WooCommerce email object.
	 */
	public function filter_reservation_payment_email_heading( $heading, $order, $email ): string {
		unset( $email );

		return $order instanceof WC_Order && self::is_deposit_order_object( $order )
			? $this->get_reservation_payment_email_label( $order )
			: (string) $heading;
	}

	/**
	 * Add reservation context before the payment item table.
	 *
	 * @param mixed $order         WooCommerce order object.
	 * @param mixed $sent_to_admin Whether the email is sent to an administrator.
	 * @param mixed $plain_text    Whether the email uses plain text.
	 * @param mixed $email         WooCommerce email object.
	 */
	public function render_reservation_payment_email_details( $order, $sent_to_admin, $plain_text, $email ): void {
		unset( $sent_to_admin, $email );

		if ( ! $order instanceof WC_Order || ! self::is_deposit_order_object( $order ) ) {
			return;
		}

		$reservation = $this->get_order_reservation( $order );
		if ( ! $reservation ) {
			return;
		}

		$rows = $this->get_reservation_payment_email_rows( $reservation );
		if ( empty( $rows ) ) {
			return;
		}

		if ( $plain_text ) {
			echo "\n" . esc_html( $this->get_reservation_payment_email_label( $order ) ) . "\n";
			foreach ( $rows as $row ) {
				echo esc_html( $row['label'] ) . ': ' . esc_html( $row['value'] ) . "\n";
			}
			echo esc_html( "\n" );
			return;
		}
		?>
		<table class="foodord-reservation-payment-email-details" role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
			<?php foreach ( $rows as $row ) : ?>
				<tr>
					<td class="foodord-reservation-payment-email-details__label"><?php echo esc_html( $row['label'] ); ?></td>
					<td class="foodord-reservation-payment-email-details__value" align="right"><?php echo esc_html( $row['value'] ); ?></td>
				</tr>
			<?php endforeach; ?>
		</table>
		<?php
	}

	/**
	 * Remove the standard order tracking action from reservation payments.
	 *
	 * @param string $url   Default tracking URL.
	 * @param mixed  $order WooCommerce order object.
	 */
	public function filter_reservation_payment_track_url( $url, $order ): string {
		return $order instanceof WC_Order && self::is_deposit_order_object( $order ) ? '' : (string) $url;
	}

	/**
	 * Append reservation payment details styles to WooCommerce emails.
	 *
	 * @param string $css   WooCommerce email CSS.
	 * @param mixed  $email WooCommerce email instance.
	 */
	public function append_reservation_payment_email_styles( $css, $email ): string {
		$order = is_object( $email ) && isset( $email->object ) && $email->object instanceof WC_Order
			? $email->object
			: false;
		if ( ! $order || ! self::is_deposit_order_object( $order ) ) {
			return (string) $css;
		}

		return (string) $css . '
.foodord-reservation-payment-email-details {
	width: 100%;
	margin: 0 0 24px;
	border-collapse: collapse;
}

.foodord-reservation-payment-email-details__label,
.foodord-reservation-payment-email-details__value {
	padding: 9px 12px;
	font-size: 14px;
}

.foodord-reservation-payment-email-details__label {
	background: #f8fafc;
	color: #697586;
}

.foodord-reservation-payment-email-details__value {
	color: #17212b;
	font-weight: 600;
	text-align: right;
}
';
	}

	/**
	 * Enqueue scoped payment screen styles.
	 */
	public function enqueue_payment_assets(): void {
		if ( ! $this->is_current_deposit_payment_request() ) {
			return;
		}

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

		wp_enqueue_script(
			'foodord-table-reservation-payment',
			FOODORD_URL . 'public/js/foodord-table-reservation-payment.js',
			array(),
			FOODORD_VERSION,
			true
		);
	}

	/**
	 * Add a body class only on reservation deposit payment screens.
	 *
	 * @param string[] $classes Body classes.
	 * @return string[]
	 */
	public function add_payment_body_class( array $classes ): array {
		if ( $this->is_current_deposit_payment_request() ) {
			$classes[] = 'foodord-reservation-deposit-payment';
		}

		return $classes;
	}

	/**
	 * Replace the generic WooCommerce order-pay endpoint title.
	 *
	 * @param string $title    Endpoint title.
	 * @param string $endpoint Endpoint key.
	 * @param string $action   Endpoint action.
	 */
	public function filter_order_pay_title( string $title, string $endpoint, string $action ): string {
		unset( $endpoint, $action );
		$order = $this->get_current_deposit_payment_order();
		if ( $order instanceof WC_Order && self::is_preorder_order_object( $order ) ) {
			return __( 'Reservation pre-order', 'foodord' );
		}

		return $this->is_current_deposit_payment_request() ? __( 'Reservation deposit', 'foodord' ) : $title;
	}

	/**
	 * Replace the generic pay button label.
	 *
	 * @param string $text Button text.
	 */
	public function filter_pay_button_text( string $text ): string {
		$order = $this->get_current_deposit_payment_order();
		if ( $order instanceof WC_Order && self::is_preorder_order_object( $order ) ) {
			return __( 'Pay now', 'foodord' );
		}

		return $this->is_current_deposit_payment_request() ? __( 'Pay deposit', 'foodord' ) : $text;
	}

	/**
	 * Prevent WooCommerce from storing its generic cancellation notice for deposit reservations.
	 *
	 * @param string $message WooCommerce cancellation notice.
	 */
	public function filter_cancelled_order_notice( string $message ): string {
		$order_id = absint( filter_input( INPUT_GET, 'order_id', FILTER_SANITIZE_NUMBER_INT ) );
		$order    = $order_id > 0 && function_exists( 'wc_get_order' ) ? wc_get_order( $order_id ) : false;

		return $order instanceof WC_Order && $this->is_deposit_order( $order ) ? '' : $message;
	}

	/**
	 * Remove a cancellation notice left by an earlier deposit payment flow.
	 */
	public function clear_stale_cancelled_order_notice(): void {
		if ( ! $this->is_current_deposit_payment_request() || ! function_exists( 'wc_get_notices' ) || ! function_exists( 'wc_set_notices' ) ) {
			return;
		}

		$notices        = wc_get_notices();
		$translations   = get_translations_for_domain( 'woocommerce' );
		$cancelled_text = wp_strip_all_tags( $translations->translate( 'Your order was cancelled.' ) );
		$changed        = false;
		foreach ( $notices as $type => $type_notices ) {
			if ( ! is_array( $type_notices ) ) {
				continue;
			}

			$filtered = array_values(
				array_filter(
					$type_notices,
					static function ( $notice ) use ( $cancelled_text ): bool {
						$notice_text = is_array( $notice ) && isset( $notice['notice'] ) ? wp_strip_all_tags( (string) $notice['notice'] ) : '';

						return $cancelled_text !== $notice_text;
					}
				)
			);

			if ( count( $filtered ) !== count( $type_notices ) ) {
				$notices[ $type ] = $filtered;
				$changed          = true;
			}
		}

		if ( $changed ) {
			wc_set_notices( $notices );
		}
	}

	/**
	 * Remove redundant subtotal row from reservation deposit payment totals.
	 *
	 * @param array<string,array{label:string,value:string}> $total_rows  Order total rows.
	 * @param WC_Order                                       $order       WooCommerce order.
	 * @param string                                         $tax_display Tax display mode.
	 * @return array<string,array{label:string,value:string}>
	 */
	public function filter_order_item_totals( array $total_rows, $order, string $tax_display ): array {
		unset( $tax_display );

		if ( $order instanceof WC_Order && $this->is_deposit_order( $order ) && ! self::is_preorder_order_object( $order ) ) {
			unset( $total_rows['cart_subtotal'] );
		}

		return $total_rows;
	}

	/**
	 * Render a compact reservation payment intro above the WooCommerce pay form.
	 *
	 * @param WC_Order $order              WooCommerce order.
	 * @param string   $order_button_text  Button text.
	 * @param array    $available_gateways Available gateways.
	 */
	public function render_payment_intro( $order, string $order_button_text, array $available_gateways ): void {
		unset( $order_button_text, $available_gateways );

		if ( ! $order instanceof WC_Order || ! $this->is_deposit_order( $order ) ) {
			return;
		}

		$reservation = $this->get_order_reservation( $order );
		$details     = $reservation ? $this->get_payment_intro_details( $reservation, $order ) : array();
		$is_preorder = self::is_preorder_order_object( $order );
		$edit_url    = $is_preorder ? self::get_preorder_edit_url( $order ) : '';
		$expires_at  = self::get_payment_expiry_gmt( $order );
		$expires_ms  = '' !== $expires_at ? strtotime( $expires_at . ' UTC' ) * 1000 : 0;
		$remaining   = $expires_ms > 0 ? max( 0, (int) ceil( ( $expires_ms - ( time() * 1000 ) ) / 1000 ) ) : 0;
		$countdown   = sprintf( '%02d:%02d', (int) floor( $remaining / 60 ), $remaining % 60 );
		$expired_url = Foodord_Table_Reservations::get_public_reservations_page_url();
		?>
		<section class="foodord-reservation-payment-intro" aria-label="<?php echo esc_attr__( 'Reservation payment summary', 'foodord' ); ?>">
			<div class="foodord-reservation-payment-intro__content">
				<?php if ( '' !== $edit_url ) : ?>
					<button class="foodord-reservation-payment-edit" type="button" data-foodord-reservation-payment-edit data-edit-url="<?php echo esc_url( $edit_url ); ?>">
						<svg class="foodord-reservation-payment-edit__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>
				<?php endif; ?>
				<span class="foodord-reservation-payment-intro__eyebrow"><?php esc_html_e( 'Table reservation', 'foodord' ); ?></span>
				<h2 class="foodord-reservation-payment-intro__title">
					<?php echo esc_html( $is_preorder ? __( 'Complete your reservation pre-order', 'foodord' ) : __( 'Complete your reservation deposit', 'foodord' ) ); ?>
				</h2>
				<p class="foodord-reservation-payment-intro__description">
					<?php echo esc_html( $is_preorder ? __( 'Complete the online payment to submit your reservation and food pre-order.', 'foodord' ) : __( 'Your table is held while you complete the online deposit payment.', 'foodord' ) ); ?>
				</p>
				<?php if ( $expires_ms > 0 ) : ?>
					<div class="foodord-reservation-payment-intro__countdown" data-foodord-reservation-payment-countdown data-expires-at="<?php echo esc_attr( (string) $expires_ms ); ?>" data-expired-redirect="<?php echo esc_url( $expired_url ); ?>">
						<span><?php esc_html_e( 'Payment time remaining', 'foodord' ); ?></span>
						<strong data-foodord-reservation-payment-countdown-value><?php echo esc_html( $countdown ); ?></strong>
					</div>
				<?php endif; ?>
				<?php if ( ! empty( $details ) ) : ?>
					<dl class="foodord-reservation-payment-intro__details">
						<?php foreach ( $details as $detail ) : ?>
							<div class="foodord-reservation-payment-intro__detail">
								<dt><?php echo esc_html( $detail['label'] ); ?></dt>
								<dd><?php echo esc_html( $detail['value'] ); ?></dd>
							</div>
						<?php endforeach; ?>
					</dl>
				<?php endif; ?>
			</div>
		</section>
		<?php
	}

	/**
	 * Render the reservation cancellation control on the deposit payment form.
	 */
	public function render_cancel_payment_control(): void {
		$order = $this->get_current_deposit_payment_order();
		if ( ! $order instanceof WC_Order ) {
			return;
		}

		$reservation = $this->get_order_reservation( $order );
		if ( ! $reservation ) {
			return;
		}

		$redirect_url = self::get_cancelled_return_url( $reservation, $order );
		$cancel_url   = $order->get_cancel_order_url( $redirect_url );
		?>
		<button class="foodord-reservation-payment-cancel" type="button" aria-controls="foodord-reservation-payment-cancel-modal" aria-expanded="false" data-foodord-reservation-payment-cancel-open>
			<?php esc_html_e( 'Cancel', 'foodord' ); ?>
		</button>
		<div id="foodord-reservation-payment-cancel-modal" class="foodord-reservation-payment-confirm" data-foodord-reservation-payment-cancel-modal hidden>
			<div class="foodord-reservation-payment-confirm__backdrop" data-foodord-reservation-payment-cancel-close></div>
			<div class="foodord-reservation-payment-confirm__dialog" role="dialog" aria-modal="true" aria-labelledby="foodord-reservation-payment-cancel-title" aria-describedby="foodord-reservation-payment-cancel-description">
				<h2 id="foodord-reservation-payment-cancel-title" class="foodord-reservation-payment-confirm__title"><?php esc_html_e( 'Cancel reservation?', 'foodord' ); ?></h2>
				<p id="foodord-reservation-payment-cancel-description" class="foodord-reservation-payment-confirm__text"><?php esc_html_e( 'This will cancel your reservation and release the table. Are you sure?', 'foodord' ); ?></p>
				<div class="foodord-reservation-payment-confirm__actions">
					<button class="foodord-reservation-payment-confirm__button foodord-reservation-payment-confirm__button--secondary" type="button" data-foodord-reservation-payment-cancel-close>
						<?php esc_html_e( 'Continue payment', 'foodord' ); ?>
					</button>
					<button class="foodord-reservation-payment-confirm__button foodord-reservation-payment-confirm__button--danger" type="button" data-foodord-reservation-payment-cancel-confirm data-cancel-url="<?php echo esc_url( $cancel_url ); ?>">
						<?php esc_html_e( 'Cancel reservation', 'foodord' ); ?>
					</button>
				</div>
			</div>
		</div>
		<?php
	}

	/**
	 * Get a cancelled reservation deposit from the public return request.
	 *
	 * @param Foodord_Table_Reservations $reservations Reservations service.
	 * @return object|null
	 */
	public static function get_cancelled_return_reservation_from_request( Foodord_Table_Reservations $reservations ) {
		$reservation_id = absint( filter_input( INPUT_GET, 'foodord_reservation_deposit_cancelled', FILTER_SANITIZE_NUMBER_INT ) );

		if ( $reservation_id <= 0 || ! function_exists( 'wc_get_order' ) ) {
			return null;
		}

		$reservation = $reservations->get_reservation( $reservation_id );
		$order_id    = $reservation && isset( $reservation->order_id ) ? absint( $reservation->order_id ) : 0;
		$order       = $order_id > 0 ? wc_get_order( $order_id ) : false;
		if ( ! $reservation || ! $order instanceof WC_Order || ! self::is_deposit_order_object( $order ) || ! $order->has_status( 'cancelled' ) ) {
			return null;
		}

		if ( ! self::cancelled_return_is_valid( $order, $reservation ) ) {
			return null;
		}

		return $reservation;
	}

	/**
	 * Redirect paid reservation deposit orders back to the reservation success screen.
	 */
	public function maybe_redirect_paid_deposit_return(): void {
		if ( ! function_exists( 'is_wc_endpoint_url' ) || ! is_wc_endpoint_url( 'order-received' ) ) {
			return;
		}

		$order_id = absint( get_query_var( 'order-received' ) );
		$order    = $order_id > 0 && function_exists( 'wc_get_order' ) ? wc_get_order( $order_id ) : false;
		if ( ! $order instanceof WC_Order || ! $this->is_deposit_order( $order ) || ! $this->return_order_key_is_valid( $order ) ) {
			return;
		}

		$reservation = $this->get_order_reservation( $order );
		if ( $reservation && $order->is_paid() && 'paid' !== (string) $reservation->payment_status ) {
			$this->handle_paid_order( $order_id );
			$reservation = $this->get_order_reservation( $order );
		}
		if ( ! $reservation || ! self::deposit_order_is_successful( $order, $reservation ) ) {
			return;
		}

		$url = self::get_paid_success_url( $reservation, $order );
		if ( '' === $url ) {
			return;
		}

		wp_safe_redirect( $url );
		exit;
	}

	/**
	 * Get a paid reservation from the public success request.
	 *
	 * @param Foodord_Table_Reservations $reservations Reservations service.
	 * @return object|null
	 */
	public static function get_paid_success_reservation_from_request( Foodord_Table_Reservations $reservations ) {
		$reservation_id = absint( filter_input( INPUT_GET, 'foodord_reservation_deposit_paid', FILTER_SANITIZE_NUMBER_INT ) );
		$order_id       = absint( filter_input( INPUT_GET, 'foodord_reservation_order', FILTER_SANITIZE_NUMBER_INT ) );
		$success_key    = filter_input( INPUT_GET, 'foodord_reservation_key' );
		$success_key    = is_string( $success_key ) ? sanitize_text_field( wp_unslash( $success_key ) ) : '';

		if ( $reservation_id <= 0 || $order_id <= 0 || '' === $success_key || ! function_exists( 'wc_get_order' ) ) {
			return null;
		}

		$order = wc_get_order( $order_id );
		if ( ! $order instanceof WC_Order || ! self::is_deposit_order_object( $order ) ) {
			return null;
		}

		$reservation = $reservations->get_reservation( $reservation_id );
		if ( ! $reservation || ! self::deposit_order_is_successful( $order, $reservation ) ) {
			return null;
		}

		if ( ! hash_equals( self::build_paid_success_key( $reservation, $order ), $success_key ) ) {
			return null;
		}

		return $reservation;
	}

	/**
	 * Check whether a reservation is attached to a pre-order payment order.
	 *
	 * @param object $reservation Reservation row.
	 */
	public static function reservation_has_preorder( $reservation ): bool {
		$order_id = isset( $reservation->order_id ) ? absint( $reservation->order_id ) : 0;
		$order    = $order_id > 0 && function_exists( 'wc_get_order' ) ? wc_get_order( $order_id ) : false;

		return $order instanceof WC_Order && self::is_preorder_order_object( $order );
	}

	/**
	 * Check whether a WooCommerce order contains a reservation pre-order.
	 *
	 * @param mixed $order WooCommerce order.
	 */
	public static function is_preorder_order( $order ): bool {
		return $order instanceof WC_Order && self::is_preorder_order_object( $order );
	}

	/**
	 * Check whether a WooCommerce order belongs to a reservation payment flow.
	 *
	 * @param mixed $order WooCommerce order.
	 */
	public static function is_reservation_payment_order( $order ): bool {
		return $order instanceof WC_Order && self::is_deposit_order_object( $order );
	}

	/**
	 * Get a signed reservation-page URL for editing an unpaid pre-order.
	 *
	 * @param WC_Order $order WooCommerce pre-order payment order.
	 */
	public static function get_preorder_edit_url( WC_Order $order ): string {
		$draft = class_exists( 'Foodord_Table_Reservation_Preorders' ) ? ( new Foodord_Table_Reservation_Preorders() )->get_payment_edit_draft_by_order( (int) $order->get_id() ) : null;
		if ( ! self::is_preorder_order_object( $order ) || ! $draft ) {
			return '';
		}

		return add_query_arg(
			array(
				'foodord_reservation_preorder_edit' => (int) $order->get_id(),
				'foodord_reservation_preorder_key'  => self::build_preorder_edit_key( $order ),
			),
			Foodord_Table_Reservations::get_public_reservations_page_url()
		);
	}

	/**
	 * Resolve a valid unpaid pre-order from a signed edit request.
	 *
	 * @param int    $order_id WooCommerce order ID.
	 * @param string $edit_key Signed edit key.
	 * @return WC_Order|false
	 */
	public static function get_editable_preorder_order( int $order_id, string $edit_key ) {
		$order = $order_id > 0 && function_exists( 'wc_get_order' ) ? wc_get_order( $order_id ) : false;
		if ( ! $order instanceof WC_Order || '' === $edit_key || ! hash_equals( self::build_preorder_edit_key( $order ), $edit_key ) ) {
			return false;
		}

		return self::is_preorder_order_editable( $order ) ? $order : false;
	}

	/**
	 * Determine whether a pre-order payment can still be edited.
	 *
	 * @param WC_Order $order WooCommerce order.
	 */
	public static function is_preorder_order_editable( WC_Order $order ): bool {
		if ( ! self::is_preorder_order_object( $order ) || $order->is_paid() || ! $order->has_status( 'pending' ) ) {
			return false;
		}

		$expires_at = self::get_payment_expiry_gmt( $order );
		if ( '' === $expires_at || strtotime( $expires_at . ' UTC' ) <= time() ) {
			return false;
		}

		$reservation_id = absint( $order->get_meta( self::META_RESERVATION_ID, true ) );
		$reservation    = $reservation_id > 0 ? ( new Foodord_Table_Reservations() )->get_reservation( $reservation_id ) : null;

		return $reservation
			&& 'paid_pending' === (string) $reservation->status
			&& 'requires_payment' === (string) $reservation->payment_status
			&& absint( $reservation->order_id ) === (int) $order->get_id();
	}

	/**
	 * Get the immutable payment-window expiry stored on an order.
	 *
	 * @param WC_Order $order WooCommerce order.
	 */
	public static function get_payment_expiry_gmt( WC_Order $order ): string {
		return sanitize_text_field( (string) $order->get_meta( self::META_EXPIRES_AT_GMT, true ) );
	}

	/**
	 * Mark an unpaid order as being edited so another tab cannot submit payment.
	 *
	 * @param WC_Order $order WooCommerce order.
	 */
	public static function begin_preorder_edit( WC_Order $order ): void {
		$order->update_meta_data( self::META_PREORDER_EDITING, '1' );
		$order->save_meta_data();
	}

	/**
	 * Return an edited order to the payable state.
	 *
	 * @param WC_Order $order WooCommerce order.
	 */
	public static function finish_preorder_edit( WC_Order $order ): void {
		$order->delete_meta_data( self::META_PREORDER_EDITING );
		$order->save_meta_data();
	}

	/**
	 * Cancel an editable pre-order payment and its reservation.
	 *
	 * @param WC_Order $order WooCommerce order.
	 */
	public static function cancel_editable_preorder_order( WC_Order $order ): bool {
		if ( ! self::is_preorder_order_editable( $order ) ) {
			return false;
		}

		self::finish_preorder_edit( $order );
		$order->update_status( 'cancelled', __( 'Reservation pre-order cancelled by the customer.', 'foodord' ) );

		return $order->has_status( 'cancelled' );
	}

	/**
	 * Build a non-reversible edit key tied to one WooCommerce order.
	 *
	 * @param WC_Order $order WooCommerce order.
	 */
	private static function build_preorder_edit_key( WC_Order $order ): string {
		return wp_hash( (int) $order->get_id() . '|' . $order->get_order_key() . '|' . $order->get_meta( self::META_RESERVATION_ID, true ) . '|preorder-edit' );
	}

	/**
	 * Build the reservation page success URL for a paid deposit.
	 *
	 * @param object   $reservation Reservation row.
	 * @param WC_Order $order       WooCommerce order.
	 */
	private static function get_paid_success_url( $reservation, WC_Order $order ): string {
		$reservation_id = isset( $reservation->id ) ? absint( $reservation->id ) : 0;
		$order_id       = (int) $order->get_id();
		if ( $reservation_id <= 0 || $order_id <= 0 ) {
			return '';
		}

		return add_query_arg(
			array(
				'foodord_reservation_deposit_paid' => $reservation_id,
				'foodord_reservation_order'        => $order_id,
				'foodord_reservation_key'          => self::build_paid_success_key( $reservation, $order ),
			),
			Foodord_Table_Reservations::get_public_reservations_page_url()
		);
	}

	/**
	 * Build the reservation page URL after a cancelled deposit payment.
	 *
	 * @param object   $reservation Reservation row.
	 * @param WC_Order $order       WooCommerce order.
	 */
	private static function get_cancelled_return_url( $reservation, WC_Order $order ): string {
		$reservation_id = isset( $reservation->id ) ? absint( $reservation->id ) : 0;
		$order_id       = (int) $order->get_id();
		if ( $reservation_id <= 0 || $order_id <= 0 ) {
			return Foodord_Table_Reservations::get_public_reservations_page_url();
		}

		return add_query_arg(
			array(
				'foodord_reservation_deposit_cancelled' => $reservation_id,
			),
			Foodord_Table_Reservations::get_public_reservations_page_url()
		);
	}

	/**
	 * Build a signed success key from reservation and order data.
	 *
	 * @param object   $reservation Reservation row.
	 * @param WC_Order $order       WooCommerce order.
	 */
	private static function build_paid_success_key( $reservation, WC_Order $order ): string {
		$reservation_id   = isset( $reservation->id ) ? absint( $reservation->id ) : 0;
		$reservation_code = isset( $reservation->reservation_code ) ? (string) $reservation->reservation_code : '';

		return wp_hash( $reservation_id . '|' . (int) $order->get_id() . '|' . $reservation_code . '|' . $order->get_order_key() . '|deposit-paid' );
	}

	/**
	 * Validate a cancelled deposit return against its order and reservation.
	 *
	 * @param WC_Order $order       WooCommerce order.
	 * @param object   $reservation Reservation row.
	 */
	private static function cancelled_return_is_valid( WC_Order $order, $reservation ): bool {
		$reservation_id = isset( $reservation->id ) ? absint( $reservation->id ) : 0;
		$order_meta_id  = absint( $order->get_meta( self::META_RESERVATION_ID, true ) );
		$res_order_id   = isset( $reservation->order_id ) ? absint( $reservation->order_id ) : 0;
		$status         = isset( $reservation->status ) ? Foodord_Table_Reservations::sanitize_status( (string) $reservation->status ) : '';
		$payment_status = isset( $reservation->payment_status ) ? sanitize_key( (string) $reservation->payment_status ) : '';

		return $reservation_id > 0
			&& $order_meta_id === $reservation_id
			&& $res_order_id === (int) $order->get_id()
			&& 'cancelled' === $status
			&& 'cancelled' === $payment_status;
	}

	/**
	 * Validate that the order and reservation represent a completed deposit payment.
	 *
	 * @param WC_Order $order       WooCommerce order.
	 * @param object   $reservation Reservation row.
	 */
	private static function deposit_order_is_successful( WC_Order $order, $reservation ): bool {
		$order_id       = (int) $order->get_id();
		$reservation_id = isset( $reservation->id ) ? absint( $reservation->id ) : 0;
		$order_meta_id  = absint( $order->get_meta( self::META_RESERVATION_ID, true ) );
		$status         = isset( $reservation->status ) ? Foodord_Table_Reservations::sanitize_status( (string) $reservation->status ) : '';
		$payment_status = isset( $reservation->payment_status ) ? sanitize_key( (string) $reservation->payment_status ) : '';
		$res_order_id   = isset( $reservation->order_id ) ? absint( $reservation->order_id ) : 0;
		$amount         = isset( $reservation->deposit_amount ) ? (float) $reservation->deposit_amount : 0.0;
		$is_preorder    = self::is_preorder_order_object( $order );

		return $reservation_id > 0
			&& $order_meta_id === $reservation_id
			&& $res_order_id === $order_id
			&& ( $amount > 0 || $is_preorder )
			&& 'paid' === $payment_status
			&& in_array( $status, array( 'pending', 'confirmed' ), true )
			&& $order->is_paid();
	}

	/**
	 * Check whether the current order-received key belongs to the order.
	 *
	 * @param WC_Order $order WooCommerce order.
	 */
	private function return_order_key_is_valid( WC_Order $order ): bool {
		$order_key = filter_input( INPUT_GET, 'key' );
		$order_key = is_string( $order_key ) ? sanitize_text_field( wp_unslash( $order_key ) ) : '';

		return '' !== $order_key && hash_equals( $order->get_order_key(), $order_key );
	}

	/**
	 * Create a WooCommerce payment order for a reservation deposit.
	 *
	 * @param object $reservation Reservation row.
	 * @return array{order_id:int,redirect_url:string}|WP_Error
	 */
	public static function create_payment_order( $reservation ) {
		if ( ! function_exists( 'wc_create_order' ) || ! class_exists( 'WC_Order_Item_Fee' ) ) {
			return new WP_Error( 'foodord_reservation_deposit_wc_missing', __( 'Online payment is not available right now.', 'foodord' ) );
		}

		$reservation_id = isset( $reservation->id ) ? absint( $reservation->id ) : 0;
		$amount         = isset( $reservation->deposit_amount ) ? (float) $reservation->deposit_amount : 0.0;
		if ( $reservation_id <= 0 || $amount <= 0 ) {
			return new WP_Error( 'foodord_reservation_invalid_deposit', __( 'Invalid reservation deposit.', 'foodord' ) );
		}

		$order = wc_create_order(
			array(
				'customer_id' => get_current_user_id(),
			)
		);

		if ( is_wp_error( $order ) || ! $order instanceof WC_Order ) {
			return new WP_Error( 'foodord_reservation_deposit_order_failed', __( 'Unable to start the deposit payment. Please try again.', 'foodord' ) );
		}

		$item = new WC_Order_Item_Fee();
		$item->set_name( __( 'Reservation deposit', 'foodord' ) );
		$item->set_amount( $amount );
		$item->set_total( $amount );
		$item->set_tax_status( 'none' );
		$order->add_item( $item );

		self::set_order_customer_data( $order, $reservation );
		$order->set_created_via( 'foodord_reservation_deposit' );
		$order->update_meta_data( self::META_DEPOSIT_ORDER, '1' );
		$order->update_meta_data( self::META_RESERVATION_ID, (string) $reservation_id );
		$order->update_meta_data( self::META_RESERVATION_CODE, isset( $reservation->reservation_code ) ? (string) $reservation->reservation_code : '' );
		$order->update_meta_data( self::META_EXPIRES_AT_GMT, gmdate( 'Y-m-d H:i:s', time() + self::PAYMENT_WINDOW_SECONDS ) );
		$order->calculate_totals( false );
		$order->save();

		$core   = new Foodord_Table_Reservations();
		$result = $core->attach_deposit_order( $reservation_id, (int) $order->get_id(), $amount );
		if ( is_wp_error( $result ) ) {
			$order->update_status( 'cancelled', __( 'Reservation deposit order could not be attached.', 'foodord' ) );
			return $result;
		}

		$redirect_url = $order->get_checkout_payment_url();
		if ( ! is_string( $redirect_url ) || '' === $redirect_url ) {
			return new WP_Error( 'foodord_reservation_deposit_payment_url_failed', __( 'Unable to start the deposit payment. Please try again.', 'foodord' ) );
		}

		return array(
			'order_id'     => (int) $order->get_id(),
			'redirect_url' => $redirect_url,
		);
	}

	/**
	 * Create one WooCommerce payment order for pre-order items and an optional deposit.
	 *
	 * @param object                         $reservation Reservation row.
	 * @param array<int,array<string,mixed>> $items       Validated pre-order items.
	 * @return array{order_id:int,redirect_url:string,expires_at_gmt:string}|WP_Error
	 */
	public static function create_preorder_payment_order( $reservation, array $items ) {
		if ( ! function_exists( 'wc_create_order' ) || ! class_exists( 'WC_Order_Item_Fee' ) || empty( $items ) ) {
			return new WP_Error( 'foodord_reservation_preorder_wc_missing', __( 'Online payment is not available right now.', 'foodord' ) );
		}

		$reservation_id = isset( $reservation->id ) ? absint( $reservation->id ) : 0;
		$deposit_amount = isset( $reservation->deposit_amount ) ? max( 0.0, (float) $reservation->deposit_amount ) : 0.0;
		if ( $reservation_id <= 0 ) {
			return new WP_Error( 'foodord_reservation_preorder_invalid', __( 'Invalid reservation pre-order.', 'foodord' ) );
		}

		$order = wc_create_order( array( 'customer_id' => get_current_user_id() ) );
		if ( is_wp_error( $order ) || ! $order instanceof WC_Order ) {
			return new WP_Error( 'foodord_reservation_preorder_order_failed', __( 'Unable to start the pre-order payment. Please try again.', 'foodord' ) );
		}

		self::set_order_customer_data( $order, $reservation );
		$added_items = self::add_preorder_product_items( $order, $items );
		if ( is_wp_error( $added_items ) ) {
			$order->delete( true );
			return $added_items;
		}

		if ( $deposit_amount > 0 ) {
			$fee = new WC_Order_Item_Fee();
			$fee->set_name( __( 'Reservation deposit', 'foodord' ) );
			$fee->set_amount( $deposit_amount );
			$fee->set_total( $deposit_amount );
			$fee->set_tax_status( 'none' );
			$order->add_item( $fee );
			$order->update_meta_data( self::META_DEPOSIT_ORDER, '1' );
		}

		$order->set_created_via( 'foodord_reservation_preorder' );
		$order->update_meta_data( self::META_PREORDER_ORDER, '1' );
		$order->update_meta_data( self::META_RESERVATION_ID, (string) $reservation_id );
		$order->update_meta_data( self::META_RESERVATION_CODE, isset( $reservation->reservation_code ) ? (string) $reservation->reservation_code : '' );
		$order->update_meta_data( self::META_EXPIRES_AT_GMT, gmdate( 'Y-m-d H:i:s', time() + self::PAYMENT_WINDOW_SECONDS ) );
		$order->calculate_totals( true );
		$order->save();

		if ( (float) $order->get_total() <= 0 ) {
			$order->delete( true );
			return new WP_Error( 'foodord_reservation_preorder_total_invalid', __( 'The pre-order total must be greater than zero.', 'foodord' ) );
		}

		$core   = new Foodord_Table_Reservations();
		$result = $core->attach_payment_order( $reservation_id, (int) $order->get_id(), $deposit_amount );
		if ( is_wp_error( $result ) ) {
			$order->update_status( 'cancelled', __( 'Reservation pre-order could not be attached.', 'foodord' ) );
			return $result;
		}

		$redirect_url = $order->get_checkout_payment_url();
		if ( ! is_string( $redirect_url ) || '' === $redirect_url ) {
			$order->update_status( 'cancelled', __( 'Reservation pre-order payment could not be started.', 'foodord' ) );
			return new WP_Error( 'foodord_reservation_preorder_payment_url_failed', __( 'Unable to start the pre-order payment. Please try again.', 'foodord' ) );
		}

		return array(
			'order_id'       => (int) $order->get_id(),
			'redirect_url'   => $redirect_url,
			'expires_at_gmt' => self::get_payment_expiry_gmt( $order ),
		);
	}

	/**
	 * Replace product lines on an existing unpaid pre-order payment.
	 *
	 * New lines are fully validated and persisted before old lines are removed.
	 *
	 * @param object                         $reservation Reservation row.
	 * @param array<int,array<string,mixed>> $items       Validated pre-order items.
	 * @return array{order_id:int,redirect_url:string,edit_url:string}|WP_Error
	 */
	public static function update_preorder_payment_order( $reservation, array $items ) {
		$order_id = isset( $reservation->order_id ) ? absint( $reservation->order_id ) : 0;
		$order    = $order_id > 0 && function_exists( 'wc_get_order' ) ? wc_get_order( $order_id ) : false;
		if ( ! $order instanceof WC_Order || empty( $items ) || ! self::is_preorder_order_editable( $order ) ) {
			return new WP_Error( 'foodord_reservation_preorder_edit_invalid', __( 'This pre-order can no longer be edited.', 'foodord' ) );
		}
		$expected_total = max( 0.0, (float) ( $reservation->deposit_amount ?? 0 ) );
		foreach ( $items as $item ) {
			$expected_total += max( 0.0, (float) ( $item['unit_price'] ?? 0 ) ) * max( 1, min( 99, absint( $item['quantity'] ?? 1 ) ) );
		}
		if ( $expected_total <= 0 ) {
			return new WP_Error( 'foodord_reservation_preorder_total_invalid', __( 'The pre-order total must be greater than zero.', 'foodord' ) );
		}

		$old_item_ids = array_map( 'absint', array_keys( $order->get_items( 'line_item' ) ) );
		foreach ( $order->get_items( 'fee' ) as $fee_item_id => $fee_item ) {
			if ( '1' === (string) $fee_item->get_meta( '_foodord_topping_tax_fee', true ) ) {
				$old_item_ids[] = absint( $fee_item_id );
			}
		}
		$new_item_ids = self::add_preorder_product_items( $order, $items );
		if ( is_wp_error( $new_item_ids ) ) {
			return $new_item_ids;
		}

		foreach ( $old_item_ids as $old_item_id ) {
			$order->remove_item( $old_item_id );
		}

		$order->calculate_totals( true );
		$order->save();

		self::finish_preorder_edit( $order );
		$redirect_url = $order->get_checkout_payment_url();

		return array(
			'order_id'     => (int) $order->get_id(),
			'redirect_url' => is_string( $redirect_url ) ? $redirect_url : '',
			'edit_url'     => self::get_preorder_edit_url( $order ),
		);
	}

	/**
	 * Add validated pre-order product lines without removing existing lines.
	 *
	 * @param WC_Order                       $order WooCommerce order.
	 * @param array<int,array<string,mixed>> $items Validated pre-order items.
	 * @return int[]|WP_Error
	 */
	private static function add_preorder_product_items( WC_Order $order, array $items ) {
		$product_modal  = class_exists( 'Foodord_Product_Modal' ) ? new Foodord_Product_Modal() : null;
		$prepared       = array();
		$tax_fee_totals = array();
		foreach ( $items as $item ) {
			$product_id   = absint( $item['product_id'] ?? 0 );
			$variation_id = absint( $item['variation_id'] ?? 0 );
			$product      = wc_get_product( $variation_id > 0 ? $variation_id : $product_id );
			$quantity     = max( 1, min( 99, absint( $item['quantity'] ?? 1 ) ) );
			if (
				! $product instanceof WC_Product
				|| ! Foodord_Table_Reservation_Preorders::is_product_enabled( $product_id )
				|| ! $product->is_in_stock()
				|| ! $product->has_enough_stock( $quantity )
			) {
				return new WP_Error( 'foodord_reservation_preorder_product_invalid', __( 'A selected product is no longer available.', 'foodord' ) );
			}

			$prepared[] = array(
				'configuration' => $item,
				'product'       => $product,
				'product_id'    => $product_id,
				'variation_id'  => $variation_id,
				'quantity'      => $quantity,
				'unit_price'    => max( 0.0, (float) ( $item['base_unit_price'] ?? $item['unit_price'] ?? 0 ) ),
			);

			$tax_fees = isset( $item['topping_tax_fees'] ) && is_array( $item['topping_tax_fees'] ) ? $item['topping_tax_fees'] : array();
			foreach ( $tax_fees as $tax_fee ) {
				if ( ! is_array( $tax_fee ) ) {
					continue;
				}
				$tax_class = self::sanitize_preorder_topping_tax_class( $tax_fee['tax_class'] ?? '' );
				$price     = max( 0.0, (float) ( $tax_fee['price'] ?? 0 ) );
				if ( '' === $tax_class || $price <= 0 ) {
					continue;
				}
				if ( ! isset( $tax_fee_totals[ $tax_class ] ) ) {
					$tax_fee_totals[ $tax_class ] = 0.0;
				}
				$tax_fee_totals[ $tax_class ] += $price * $quantity;
			}
		}

		$added_item_ids = array();
		foreach ( $prepared as $prepared_item ) {
			$line_total    = $prepared_item['unit_price'] * $prepared_item['quantity'];
			$order_item_id = $order->add_product(
				$prepared_item['product'],
				$prepared_item['quantity'],
				array(
					'subtotal' => $line_total,
					'total'    => $line_total,
				)
			);
			if ( ! $order_item_id ) {
				foreach ( $added_item_ids as $added_item_id ) {
					$order->remove_item( $added_item_id );
				}
				$order->save();
				return new WP_Error( 'foodord_reservation_preorder_product_failed', __( 'Unable to add a selected product to the payment order.', 'foodord' ) );
			}

			$added_item_ids[] = (int) $order_item_id;
			$order_item       = $order->get_item( $order_item_id );
			$item             = $prepared_item['configuration'];
			if ( $order_item instanceof WC_Order_Item_Product && $product_modal instanceof Foodord_Product_Modal ) {
				$product_modal->add_toppings_to_order_line_item(
					$order_item,
					'',
					array(
						'product_id'                  => $prepared_item['product_id'],
						'variation_id'                => $prepared_item['variation_id'],
						'foodord_toppings'            => isset( $item['toppings'] ) && is_array( $item['toppings'] ) ? $item['toppings'] : array(),
						'foodord_toppings_extra'      => max( 0.0, (float) ( $item['toppings_extra'] ?? 0 ) ),
						'foodord_topping_tax_fees'    => isset( $item['topping_tax_fees'] ) && is_array( $item['topping_tax_fees'] ) ? $item['topping_tax_fees'] : array(),
						'foodord_toppings_signature'  => sanitize_text_field( (string) ( $item['toppings_signature'] ?? '' ) ),
						'foodord_toppings_base_price' => max( 0.0, $prepared_item['unit_price'] - (float) ( $item['toppings_extra'] ?? 0 ) ),
						'foodord_note'                => sanitize_textarea_field( (string) ( $item['note'] ?? '' ) ),
					),
					$order
				);
				$order_item->save();
			}
		}

		foreach ( $tax_fee_totals as $tax_class => $gross_amount ) {
			$tax_label = self::get_preorder_topping_tax_class_label( $tax_class );
			// translators: %s is a WooCommerce tax class name.
			$fee_name  = sprintf( __( 'Toppings - %s', 'foodord' ), $tax_label );
			$fee       = new WC_Order_Item_Fee();
			$net_total = self::get_preorder_topping_fee_net_amount( $order, $gross_amount, $tax_class );
			$fee->set_name( $fee_name );
			$fee->set_amount( $net_total );
			$fee->set_total( $net_total );
			$fee->set_tax_status( wc_tax_enabled() ? 'taxable' : 'none' );
			$fee->set_tax_class( 'standard' === $tax_class ? '' : $tax_class );
			$fee->add_meta_data( '_foodord_topping_tax_fee', '1', true );
			$fee->add_meta_data( '_foodord_topping_tax_class', $tax_class, true );
			$order->add_item( $fee );
		}

		return $added_item_ids;
	}

	/**
	 * Sanitize a pre-order topping tax class.
	 *
	 * @param mixed $tax_class Tax class value.
	 */
	private static function sanitize_preorder_topping_tax_class( $tax_class ): string {
		$tax_class = sanitize_title( (string) $tax_class );
		if ( '' === $tax_class || 'standard' === $tax_class ) {
			return $tax_class;
		}
		$allowed = class_exists( 'WC_Tax' ) ? array_map( 'sanitize_title', WC_Tax::get_tax_class_slugs() ) : array();
		return in_array( $tax_class, $allowed, true ) ? $tax_class : '';
	}

	/**
	 * Get a readable pre-order topping tax class label.
	 *
	 * @param string $tax_class Internal tax class.
	 */
	private static function get_preorder_topping_tax_class_label( string $tax_class ): string {
		if ( 'standard' === $tax_class ) {
			return __( 'Standard', 'foodord' );
		}
		if ( class_exists( 'WC_Tax' ) ) {
			$names = WC_Tax::get_tax_classes();
			$slugs = array_map( 'sanitize_title', WC_Tax::get_tax_class_slugs() );
			$key   = array_search( $tax_class, $slugs, true );
			if ( false !== $key && isset( $names[ $key ] ) ) {
				return sanitize_text_field( (string) $names[ $key ] );
			}
		}
		return sanitize_text_field( $tax_class );
	}

	/**
	 * Convert a tax-inclusive topping amount to an order fee net amount.
	 *
	 * @param WC_Order $order        Payment order.
	 * @param float    $gross_amount Configured topping amount.
	 * @param string   $tax_class    Internal tax class.
	 */
	private static function get_preorder_topping_fee_net_amount( WC_Order $order, float $gross_amount, string $tax_class ): float {
		$gross_amount = max( 0.0, $gross_amount );
		if ( ! wc_tax_enabled() || ! wc_prices_include_tax() || ! class_exists( 'WC_Tax' ) ) {
			return $gross_amount;
		}

		$base_location = function_exists( 'wc_get_base_location' )
			? wc_get_base_location()
			: array(
				'country' => '',
				'state'   => '',
			);
		$country       = $order->get_billing_country();
		$state         = $order->get_billing_state();
		$rates         = WC_Tax::find_rates(
			array(
				'country'   => '' !== $country ? $country : ( $base_location['country'] ?? '' ),
				'state'     => '' !== $state ? $state : ( $base_location['state'] ?? '' ),
				'postcode'  => $order->get_billing_postcode(),
				'city'      => $order->get_billing_city(),
				'tax_class' => 'standard' === $tax_class ? '' : $tax_class,
			)
		);
		$taxes         = WC_Tax::calc_tax( $gross_amount, $rates, true );
		return max( 0.0, $gross_amount - array_sum( $taxes ) );
	}

	/**
	 * Remove offline gateways for reservation deposit orders.
	 *
	 * @param array<string,WC_Payment_Gateway> $gateways Available gateways.
	 * @return array<string,WC_Payment_Gateway>
	 */
	public function filter_payment_gateways( array $gateways ): array {
		if ( ! $this->is_current_deposit_payment_request() ) {
			return $gateways;
		}

		$offline_gateways = apply_filters( 'foodord_reservation_deposit_offline_gateway_ids', self::OFFLINE_GATEWAY_IDS );
		$offline_gateways = is_array( $offline_gateways ) ? array_map( 'sanitize_key', $offline_gateways ) : self::OFFLINE_GATEWAY_IDS;
		foreach ( $offline_gateways as $gateway_id ) {
			unset( $gateways[ $gateway_id ] );
		}

		return $gateways;
	}

	/**
	 * Prevent late payments for expired reservation deposit orders.
	 *
	 * @param string[] $statuses Valid order statuses.
	 * @param WC_Order $order    WooCommerce order.
	 * @return string[]
	 */
	public function filter_valid_order_statuses_for_payment( array $statuses, $order ): array {
		if ( ! $order instanceof WC_Order || ! $this->is_deposit_order( $order ) ) {
			return $statuses;
		}
		if ( self::is_preorder_order_object( $order ) && '1' === (string) $order->get_meta( self::META_PREORDER_EDITING, true ) ) {
			return array();
		}

		$reservation = $this->get_order_reservation( $order );
		if ( $reservation && $this->reservations->is_deposit_payment_expired( $reservation ) ) {
			$this->reservations->expire_unpaid_deposit_reservations();
			return array();
		}

		return $statuses;
	}

	/**
	 * Handle successful deposit payment.
	 *
	 * @param int $order_id WooCommerce order ID.
	 */
	public function handle_paid_order( int $order_id ): void {
		$order = function_exists( 'wc_get_order' ) ? wc_get_order( $order_id ) : false;
		if ( ! $order instanceof WC_Order || ! $this->is_deposit_order( $order ) || ! $order->is_paid() ) {
			return;
		}

		$reservation = $this->get_order_reservation( $order );
		if ( ! $reservation ) {
			return;
		}

		if ( 'paid' === (string) $reservation->payment_status ) {
			return;
		}

		$date_paid      = $order->get_date_paid();
		$paid_timestamp = $date_paid instanceof WC_DateTime ? $date_paid->getTimestamp() : time();
		$status         = $this->reservations->mark_deposit_paid( (int) $reservation->id, $paid_timestamp );
		if ( is_wp_error( $status ) ) {
			$reservation = $this->reservations->get_reservation( (int) $reservation->id );
			if ( $reservation && $this->payment_requires_manual_review( $order, $reservation, $paid_timestamp ) ) {
				$this->flag_payment_for_manual_review( $order );
				return;
			}
			$order->add_order_note( $status->get_error_message() );
			return;
		}
		if ( false === $status ) {
			return;
		}

		$order->delete_meta_data( self::META_REVIEW_REQUIRED );
		$order->save_meta_data();

		$reservation = $this->reservations->get_reservation( (int) $reservation->id );
		if ( ! $reservation ) {
			return;
		}

		$manage_token = $this->reservations->prepare_manage_token( (int) $reservation->id );
		if ( '' !== $manage_token ) {
			$reservation->manage_token = $manage_token;
		}

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

		$this->emails->send_admin_notification_email( $reservation );
		$order->add_order_note( self::is_preorder_order_object( $order ) ? __( 'Reservation pre-order payment completed.', 'foodord' ) : __( 'Reservation deposit payment completed.', 'foodord' ) );
		if ( self::is_preorder_order_object( $order ) && class_exists( 'Foodord_Table_Reservation_Preorders' ) ) {
			Foodord_Table_Reservation_Preorders::delete_draft_by_order_id( $order_id );
		}
	}

	/**
	 * Check whether a completed payment needs manual review instead of confirmation.
	 *
	 * @param WC_Order $order          WooCommerce order.
	 * @param object   $reservation    Reservation row.
	 * @param int      $paid_timestamp Payment completion timestamp.
	 */
	private function payment_requires_manual_review( WC_Order $order, $reservation, int $paid_timestamp ): bool {
		$status         = Foodord_Table_Reservations::sanitize_status( (string) ( $reservation->status ?? '' ) );
		$payment_status = sanitize_key( (string) ( $reservation->payment_status ?? '' ) );
		if ( 'cancelled' === $status || in_array( $payment_status, array( 'expired', 'cancelled' ), true ) ) {
			return true;
		}

		$expires_at = self::get_payment_expiry_gmt( $order );
		$expires_ts = '' !== $expires_at ? strtotime( $expires_at . ' UTC' ) : false;

		return false !== $expires_ts && $paid_timestamp > $expires_ts;
	}

	/**
	 * Record an idempotent admin warning for a paid order that cannot be confirmed.
	 *
	 * @param WC_Order $order WooCommerce order.
	 */
	private function flag_payment_for_manual_review( WC_Order $order ): void {
		if ( '1' === (string) $order->get_meta( self::META_REVIEW_REQUIRED, true ) ) {
			return;
		}

		$order->update_meta_data( self::META_REVIEW_REQUIRED, '1' );
		$order->save_meta_data();
		$order->add_order_note(
			__( 'Reservation payment was received after the reservation could no longer be confirmed. The reservation was not confirmed. Review the order and issue a manual refund if required.', 'foodord' )
		);
	}

	/**
	 * Handle failed or cancelled deposit payment orders.
	 *
	 * @param int $order_id WooCommerce order ID.
	 */
	public function handle_failed_order( int $order_id ): void {
		$order = function_exists( 'wc_get_order' ) ? wc_get_order( $order_id ) : false;
		if ( ! $order instanceof WC_Order || ! $this->is_deposit_order( $order ) ) {
			return;
		}

		$reservation = $this->get_order_reservation( $order );
		if ( $reservation ) {
			$payment_status = $order->has_status( 'cancelled' ) ? 'cancelled' : 'failed';
			$this->reservations->mark_deposit_payment_failed( (int) $reservation->id, $payment_status );
		}
		if ( self::is_preorder_order_object( $order ) && class_exists( 'Foodord_Table_Reservation_Preorders' ) ) {
			Foodord_Table_Reservation_Preorders::delete_draft_by_order_id( $order_id );
		}
	}

	/**
	 * Cleanup expired unpaid deposit reservations.
	 */
	public function cleanup_expired_deposits(): void {
		$this->reservations->expire_unpaid_deposit_reservations();
	}

	/**
	 * Build payment intro detail rows.
	 *
	 * @param object   $reservation Reservation row.
	 * @param WC_Order $order       WooCommerce order.
	 * @return array<int,array{label:string,value:string}>
	 */
	private function get_payment_intro_details( $reservation, WC_Order $order ): array {
		$details = array();
		$code    = isset( $reservation->reservation_code ) ? trim( (string) $reservation->reservation_code ) : '';
		$date    = $this->format_reservation_date( $reservation );
		$time    = $this->format_reservation_time( $reservation );
		$guests  = isset( $reservation->party_size ) ? absint( $reservation->party_size ) : 0;

		if ( '' !== $code ) {
			$details[] = array(
				'label' => __( 'Reservation code', 'foodord' ),
				'value' => $code,
			);
		}

		if ( '' !== $date ) {
			$details[] = array(
				'label' => __( 'Date', 'foodord' ),
				'value' => $date,
			);
		}

		if ( '' !== $time ) {
			$details[] = array(
				'label' => __( 'Time', 'foodord' ),
				'value' => $time,
			);
		}

		if ( $guests > 0 ) {
			$details[] = array(
				'label' => __( 'Guests', 'foodord' ),
				'value' => (string) $guests,
			);
		}

		if ( ! self::is_preorder_order_object( $order ) ) {
			$details[] = array(
				'label' => __( 'Deposit amount', 'foodord' ),
				'value' => html_entity_decode( wp_strip_all_tags( $order->get_formatted_order_total() ), ENT_QUOTES, 'UTF-8' ),
			);
		}

		return $details;
	}

	/**
	 * Build reservation context rows for WooCommerce payment emails.
	 *
	 * @param object $reservation Reservation row.
	 * @return array<int,array{label:string,value:string}>
	 */
	private function get_reservation_payment_email_rows( $reservation ): array {
		$rows       = array();
		$date       = $this->format_reservation_date( $reservation );
		$time       = $this->format_reservation_time( $reservation );
		$party_size = absint( $reservation->party_size ?? 0 );
		$table_name = trim( wp_strip_all_tags( (string) ( $reservation->table_name ?? '' ) ) );
		$floor_name = trim( wp_strip_all_tags( (string) ( $reservation->floor_name ?? '' ) ) );
		$table      = implode( ' - ', array_filter( array( $floor_name, $table_name ) ) );
		$code       = trim( wp_strip_all_tags( (string) ( $reservation->reservation_code ?? '' ) ) );

		if ( '' !== $date ) {
			$rows[] = array(
				'label' => __( 'Date', 'foodord' ),
				'value' => $date,
			);
		}

		if ( '' !== $time ) {
			$rows[] = array(
				'label' => __( 'Time', 'foodord' ),
				'value' => $time,
			);
		}

		if ( $party_size > 0 ) {
			$rows[] = array(
				'label' => __( 'Guests', 'foodord' ),
				'value' => (string) $party_size,
			);
		}

		if ( '' !== $table ) {
			$rows[] = array(
				'label' => __( 'Table', 'foodord' ),
				'value' => $table,
			);
		}

		if ( '' !== $code ) {
			$rows[] = array(
				'label' => __( 'Reservation code', 'foodord' ),
				'value' => $code,
			);
		}

		return $rows;
	}

	/**
	 * Get the email label for a reservation payment order.
	 *
	 * @param WC_Order $order WooCommerce order.
	 */
	private function get_reservation_payment_email_label( WC_Order $order ): string {
		return self::is_preorder_order_object( $order )
			? __( 'Reservation pre-order', 'foodord' )
			: __( 'Reservation deposit', 'foodord' );
	}

	/**
	 * Format reservation date in the WordPress date format.
	 *
	 * @param object $reservation Reservation row.
	 */
	private function format_reservation_date( $reservation ): string {
		$date = isset( $reservation->reservation_date ) ? (string) $reservation->reservation_date : '';
		if ( '' === $date ) {
			return '';
		}

		$datetime = DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $date . ' 00:00:00', wp_timezone() );

		return $datetime instanceof DateTimeImmutable ? wp_date( get_option( 'date_format' ), $datetime->getTimestamp() ) : '';
	}

	/**
	 * Format reservation time in the WordPress time format.
	 *
	 * @param object $reservation Reservation row.
	 */
	private function format_reservation_time( $reservation ): string {
		$date = isset( $reservation->reservation_date ) ? (string) $reservation->reservation_date : '1970-01-01';
		$time = isset( $reservation->reservation_time ) ? (string) $reservation->reservation_time : '';
		if ( '' === $time ) {
			return '';
		}

		$datetime = DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $date . ' ' . $time, wp_timezone() );

		return $datetime instanceof DateTimeImmutable ? wp_date( get_option( 'time_format' ), $datetime->getTimestamp() ) : '';
	}

	/**
	 * Set order billing fields from reservation customer data.
	 *
	 * @param WC_Order $order       WooCommerce order.
	 * @param object   $reservation Reservation row.
	 */
	private static function set_order_customer_data( WC_Order $order, $reservation ): void {
		$name  = isset( $reservation->customer_name ) ? trim( wp_strip_all_tags( (string) $reservation->customer_name ) ) : '';
		$email = isset( $reservation->customer_email ) ? sanitize_email( (string) $reservation->customer_email ) : '';
		$phone = isset( $reservation->customer_phone ) ? sanitize_text_field( (string) $reservation->customer_phone ) : '';
		$parts = preg_split( '/\s+/', $name, 2 );
		$first = is_array( $parts ) && isset( $parts[0] ) ? $parts[0] : $name;
		$last  = is_array( $parts ) && isset( $parts[1] ) ? $parts[1] : '';

		$order->set_billing_first_name( $first );
		$order->set_billing_last_name( $last );
		if ( is_email( $email ) ) {
			$order->set_billing_email( $email );
		}
		$order->set_billing_phone( $phone );
	}

	/**
	 * Check whether the current request is paying a reservation deposit order.
	 */
	private function is_current_deposit_payment_request(): bool {
		return $this->get_current_deposit_payment_order() instanceof WC_Order;
	}

	/**
	 * Get the deposit order from the current payment request.
	 *
	 * @return WC_Order|false
	 */
	private function get_current_deposit_payment_order() {
		$order_id = absint( get_query_var( 'order-pay' ) );
		if ( $order_id <= 0 ) {
			$order_id = absint( filter_input( INPUT_GET, 'order-pay', FILTER_SANITIZE_NUMBER_INT ) );
		}

		$order = $order_id > 0 && function_exists( 'wc_get_order' ) ? wc_get_order( $order_id ) : false;

		return $order instanceof WC_Order && $this->is_deposit_order( $order ) ? $order : false;
	}

	/**
	 * Check whether an order belongs to a reservation payment flow.
	 *
	 * @param WC_Order $order WooCommerce order.
	 */
	private function is_deposit_order( WC_Order $order ): bool {
		return self::is_deposit_order_object( $order );
	}

	/**
	 * Check whether an order is a reservation deposit order.
	 *
	 * @param WC_Order $order WooCommerce order.
	 */
	private static function is_deposit_order_object( WC_Order $order ): bool {
		return '1' === (string) $order->get_meta( self::META_DEPOSIT_ORDER, true ) || self::is_preorder_order_object( $order );
	}

	/**
	 * Check whether an order contains a reservation pre-order.
	 *
	 * @param WC_Order $order WooCommerce order.
	 */
	private static function is_preorder_order_object( WC_Order $order ): bool {
		return '1' === (string) $order->get_meta( self::META_PREORDER_ORDER, true );
	}

	/**
	 * Get the reservation attached to a deposit order.
	 *
	 * @param WC_Order $order WooCommerce order.
	 * @return object|null
	 */
	private function get_order_reservation( WC_Order $order ) {
		$reservation_id = absint( $order->get_meta( self::META_RESERVATION_ID, true ) );

		return $reservation_id > 0 ? $this->reservations->get_reservation( $reservation_id ) : null;
	}
}
