<?php
/**
 * FoodOrd Product Modal Class
 *
 * This class handles all functionality related to the product modal system.
 *
 * @package FoodOrd
 * @version 1.2
 */

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

/**
 * FoodOrd Product Modal class
 */
class Foodord_Product_Modal {

	/**
	 * Maximum allowed length for cart item notes.
	 */
	private const CART_NOTE_MAX_LENGTH = 160;

	/**
	 * Upsell helper.
	 *
	 * @var Foodord_Product_Modal_Upsells|null
	 */
	private $modal_upsells = null;

	/**
	 * Cart upsell renderer.
	 *
	 * @var Foodord_Card_Upsells|null
	 */
	private $card_upsells = null;

	/**
	 * Cart fees temporarily hidden while WooCommerce renders totals tables.
	 *
	 * @var array|null
	 */
	private $render_hidden_topping_tax_fees = null;

	/**
	 * Constructor
	 */
	public function __construct() {
		if ( class_exists( 'Foodord_Product_Modal_Upsells' ) ) {
			$this->modal_upsells = new Foodord_Product_Modal_Upsells();
		}

		if ( class_exists( 'Foodord_Card_Upsells' ) ) {
			$this->card_upsells = new Foodord_Card_Upsells();
		}
	}

	/**
	 * Initialize the modal system
	 */
	public function init() {
		// Enqueue scripts and styles.
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_scripts' ) );

		// AJAX handlers.
		add_action( 'wp_ajax_foodord_get_product_modal', array( $this, 'ajax_get_product_modal' ) );
		add_action( 'wp_ajax_nopriv_foodord_get_product_modal', array( $this, 'ajax_get_product_modal' ) );

		// Cart AJAX handlers.
		add_action( 'wp_ajax_foodord_update_cart_quantity', array( $this, 'ajax_update_cart_quantity' ) );
		add_action( 'wp_ajax_nopriv_foodord_update_cart_quantity', array( $this, 'ajax_update_cart_quantity' ) );
		add_action( 'wp_ajax_foodord_remove_cart_item', array( $this, 'ajax_remove_cart_item' ) );
		add_action( 'wp_ajax_nopriv_foodord_remove_cart_item', array( $this, 'ajax_remove_cart_item' ) );
		add_action( 'wp_ajax_foodord_clear_cart', array( $this, 'ajax_clear_cart' ) );
		add_action( 'wp_ajax_nopriv_foodord_clear_cart', array( $this, 'ajax_clear_cart' ) );
		add_action( 'wp_ajax_foodord_get_cart_items', array( $this, 'ajax_get_cart_items' ) );
		add_action( 'wp_ajax_nopriv_foodord_get_cart_items', array( $this, 'ajax_get_cart_items' ) );
		add_action( 'wp_ajax_foodord_update_cart_item_note', array( $this, 'ajax_update_cart_item_note' ) );
		add_action( 'wp_ajax_nopriv_foodord_update_cart_item_note', array( $this, 'ajax_update_cart_item_note' ) );

		// Add to cart AJAX handler.
		add_action( 'wp_ajax_foodord_add_to_cart', array( $this, 'ajax_add_to_cart' ) );
		add_action( 'wp_ajax_nopriv_foodord_add_to_cart', array( $this, 'ajax_add_to_cart' ) );

		// WooCommerce cart integration.
		add_filter( 'woocommerce_add_cart_item_data', array( $this, 'filter_add_cart_item_data' ), 10, 3 );
		add_filter( 'woocommerce_get_cart_item_from_session', array( $this, 'restore_cart_item_from_session' ), 10, 3 );
		add_filter( 'woocommerce_add_cart_item', array( $this, 'apply_toppings_to_cart_item' ), 10, 2 );
		add_action( 'woocommerce_before_calculate_totals', array( $this, 'enforce_toppings_pricing' ), 15 );
		add_action( 'woocommerce_cart_calculate_fees', array( $this, 'apply_topping_tax_fees' ), 12 );
		add_filter( 'woocommerce_get_item_data', array( $this, 'expose_topping_item_data' ), 10, 2 );
		add_action( 'woocommerce_review_order_after_cart_contents', array( $this, 'hide_topping_tax_fees_from_totals' ), 1 );
		add_action( 'woocommerce_review_order_before_order_total', array( $this, 'restore_topping_tax_fees_after_totals' ), 1 );
		add_action( 'woocommerce_before_cart_totals', array( $this, 'hide_topping_tax_fees_from_totals' ), 1 );
		add_action( 'woocommerce_cart_totals_before_order_total', array( $this, 'restore_topping_tax_fees_after_totals' ), 1 );
		add_filter( 'woocommerce_cart_totals_order_total_html', array( $this, 'format_order_total_tax_breakdown' ), 20 );
		add_filter( 'woocommerce_get_order_item_totals', array( $this, 'hide_topping_tax_fees_from_order_totals' ), 20, 2 );
		add_filter( 'rest_request_after_callbacks', array( $this, 'hide_topping_tax_fees_from_store_api' ), 20, 3 );
		add_filter( 'woocommerce_hydration_request_after_callbacks', array( $this, 'hide_topping_tax_fees_from_store_api' ), 20, 3 );

		// Additional WooCommerce hooks for better toppings integration.
		add_filter( 'woocommerce_cart_item_price', array( $this, 'display_cart_item_price_with_toppings' ), 10, 3 );
		add_filter( 'woocommerce_cart_item_subtotal', array( $this, 'display_cart_item_subtotal_with_toppings' ), 10, 3 );
		add_action( 'woocommerce_checkout_create_order_line_item', array( $this, 'add_toppings_to_order_line_item' ), 10, 4 );
		add_action( 'woocommerce_checkout_create_order_fee_item', array( $this, 'mark_topping_tax_order_fee' ), 10, 4 );

		if ( ! $this->is_foodord_theme_handling_order_item_meta() ) {
			add_filter( 'woocommerce_order_item_get_formatted_meta_data', array( $this, 'filter_internal_order_item_meta' ), 10, 2 );
			add_action( 'woocommerce_order_item_meta_end', array( $this, 'render_public_order_item_toppings' ), 15, 4 );
		}
		if ( ! function_exists( 'foodord_render_public_order_item_note' ) ) {
			add_action( 'woocommerce_order_item_meta_end', array( $this, 'render_public_order_item_note' ), 25, 4 );
		}
		if ( ! function_exists( 'foodord_render_admin_order_item_note' ) ) {
			add_action( 'woocommerce_after_order_itemmeta', array( $this, 'render_admin_order_item_note' ), 25, 3 );
		}
		add_filter( 'woocommerce_email_styles', array( $this, 'append_preorder_item_meta_email_styles' ), 20, 2 );
	}

	/**
	 * Enqueue scripts and styles
	 */
	public function enqueue_scripts() {
		// Don't load on admin pages.
		if ( is_admin() ) {
			return;
		}

		// For testing, always load on frontend.
		$should_load = true;

		if ( function_exists( 'foodord_enqueue_touch_styles' ) ) {
			foodord_enqueue_touch_styles();
		}

		// Enqueue CSS.
		wp_enqueue_style(
			'foodord-product-modal',
			FOODORD_URL . 'public/css/foodord-product-modal.css',
			array( 'foodord-touch' ),
			FOODORD_VERSION,
			'all'
		);

		if ( ( function_exists( 'is_cart' ) && is_cart() ) || ( function_exists( 'is_checkout' ) && is_checkout() ) ) {
			wp_enqueue_style(
				'foodord-checkout-toppings',
				FOODORD_URL . 'public/css/foodord-checkout-toppings.css',
				array(),
				FOODORD_VERSION,
				'all'
			);
		}

		if ( function_exists( 'foodord_enqueue_lazy_image_script' ) ) {
			foodord_enqueue_lazy_image_script();
		}

		wp_enqueue_script(
			'foodord-cart-notes',
			FOODORD_URL . 'public/js/foodord-cart-notes.js',
			array(),
			FOODORD_VERSION,
			true
		);

		wp_localize_script(
			'foodord-cart-notes',
			'foodordCartNotesLabels',
			function_exists( 'foodord_get_cart_note_labels' ) ? foodord_get_cart_note_labels() : array()
		);

		wp_enqueue_script(
			'foodord-upsell-skeleton',
			FOODORD_URL . 'public/js/foodord-upsell-skeleton.js',
			array(),
			FOODORD_VERSION,
			true
		);

		// Enqueue modal footer JavaScript first.
		wp_enqueue_script(
			'foodord-modal-footer',
			FOODORD_URL . 'public/js/foodord-modal-footer.js',
			array( 'jquery', 'foodord-cart-notes' ),
			FOODORD_VERSION,
			true
		);

		wp_localize_script(
			'foodord-modal-footer',
			'foodordModalFooterI18n',
			array(
				'checkout'                          => __( 'Checkout', 'foodord' ),
				'viewCart'                          => __( 'View Cart', 'foodord' ),
				'emptyCartText'                     => __( 'Your cart is empty', 'foodord' ),
				'emptyCartSubtext'                  => __( 'Add items to get started', 'foodord' ),
				'itemsInCart'                       => __( 'Items in cart', 'foodord' ),
				'subtotal'                          => __( 'Subtotal', 'foodord' ),
				'deliveryFee'                       => __( 'Delivery Fee', 'foodord' ),
				'requiredSelectionsBeforeAddToCart' => __( 'Please complete the required selections before adding to cart.', 'foodord' ),
				'adding'                            => __( 'Adding…', 'foodord' ),
				'addToCart'                         => __( 'Add to cart', 'foodord' ),
				/* translators: %s: Product name */
				'editItem'                          => __( 'Edit %s', 'foodord' ),
				'updateItem'                        => __( 'Update item', 'foodord' ),
			)
		);

		// Enqueue cart popover JavaScript.
		wp_enqueue_script(
			'foodord-cart-popover',
			FOODORD_URL . 'public/js/foodord-cart-popover.js',
			array( 'jquery' ),
			FOODORD_VERSION,
			true
		);

		// Enqueue toppings JavaScript.
		wp_enqueue_script(
			'foodord-toppings',
			FOODORD_URL . 'public/js/foodord-toppings.js',
			array( 'jquery' ),
			FOODORD_VERSION,
			true
		);

		// Enqueue main modal JavaScript.
		wp_enqueue_script(
			'foodord-swipe-dismiss',
			FOODORD_URL . 'public/js/foodord-swipe-dismiss.js',
			array(),
			FOODORD_VERSION,
			true
		);

		wp_enqueue_script(
			'foodord-product-modal',
			FOODORD_URL . 'public/js/foodord-product-modal.js',
			array( 'jquery', 'foodord-modal-footer', 'foodord-toppings', 'foodord-upsell-skeleton', 'foodord-swipe-dismiss' ),
			FOODORD_VERSION,
			true
		);

		// Ensure WooCommerce variation add-to-cart script is available for variable products inside modal.
		if ( function_exists( 'wp_enqueue_script' ) ) {
			wp_enqueue_script( 'wc-add-to-cart-variation' );
		}

		$cart_summary              = $this->build_cart_summary_payload();
		$localized_cart_items      = isset( $cart_summary['cart_items'] ) ? $cart_summary['cart_items'] : array();
		$localized_cart_total      = isset( $cart_summary['cart_total_html'] ) ? $cart_summary['cart_total_html'] : ( ( function_exists( 'WC' ) && WC()->cart ) ? WC()->cart->get_cart_total() : '' );
		$show_topping_group_labels = isset( $cart_summary['show_topping_group_labels'] ) ? $cart_summary['show_topping_group_labels'] : ( 'yes' === get_option( 'foodord_show_topping_group_label', 'yes' ) );
		$modal_upsells_enabled     = 'yes' === get_option( 'foodord_enable_modal_upsells', 'no' );

		// Localize script for modal.
		wp_localize_script(
			'foodord-product-modal',
			'foodord_modal_vars',
			array(
				'ajax_url'                  => admin_url( 'admin-ajax.php' ),
				'wc_ajax_url'               => class_exists( 'WC_AJAX' ) ? WC_AJAX::get_endpoint( '%%endpoint%%' ) : '',
				'nonce'                     => wp_create_nonce( 'foodord_product_modal_nonce' ),
				'cart_url'                  => function_exists( 'wc_get_cart_url' ) ? wc_get_cart_url() : '/cart',
				'checkout_url'              => function_exists( 'wc_get_checkout_url' ) ? wc_get_checkout_url() : '/checkout',
				'is_checkout_page'          => function_exists( 'is_checkout' ) ? is_checkout() : false,
				'checkout_btn_text'         => function_exists( 'is_checkout' ) && is_checkout() ? __( 'Order and Pay', 'foodord' ) : __( 'Checkout', 'foodord' ),
				'cart_items'                => $localized_cart_items,
				'cart_total_html'           => $localized_cart_total,
				'cart_zero_html'            => function_exists( 'wc_price' ) ? wc_price( 0 ) : '0',
				'currency_symbol'           => function_exists( 'get_woocommerce_currency_symbol' ) ? get_woocommerce_currency_symbol() : '',
				'currency_pos'              => get_option( 'woocommerce_currency_pos', 'left' ),
				'price_decimals'            => function_exists( 'wc_get_price_decimals' ) ? wc_get_price_decimals() : 2,
				'price_decimal_sep'         => function_exists( 'wc_get_price_decimal_separator' ) ? wc_get_price_decimal_separator() : '.',
				'price_thousand_sep'        => function_exists( 'wc_get_price_thousand_separator' ) ? wc_get_price_thousand_separator() : ',',
				'note_max_length'           => self::CART_NOTE_MAX_LENGTH,
				'show_topping_group_labels' => $show_topping_group_labels,
				'modal_upsells_enabled'     => $modal_upsells_enabled,
				'i18n'                      => array(
					'loading_product' => __( 'Loading product...', 'foodord' ),
					'added_to_cart'   => __( 'Added to cart', 'foodord' ),
				),
			)
		);

		// Localize script for cart popover.
		wp_localize_script(
			'foodord-cart-popover',
			'foodord_modal_vars',
			array(
				'ajax_url'                  => admin_url( 'admin-ajax.php' ),
				'wc_ajax_url'               => class_exists( 'WC_AJAX' ) ? WC_AJAX::get_endpoint( '%%endpoint%%' ) : '',
				'nonce'                     => wp_create_nonce( 'foodord_product_modal_nonce' ),
				'cart_url'                  => function_exists( 'wc_get_cart_url' ) ? wc_get_cart_url() : '/cart',
				'checkout_url'              => function_exists( 'wc_get_checkout_url' ) ? wc_get_checkout_url() : '/checkout',
				'is_checkout_page'          => function_exists( 'is_checkout' ) ? is_checkout() : false,
				'checkout_btn_text'         => function_exists( 'is_checkout' ) && is_checkout() ? __( 'Order and Pay', 'foodord' ) : __( 'Checkout', 'foodord' ),
				'cart_items'                => $localized_cart_items,
				'cart_total_html'           => $localized_cart_total,
				'cart_zero_html'            => function_exists( 'wc_price' ) ? wc_price( 0 ) : '0',
				'currency_symbol'           => function_exists( 'get_woocommerce_currency_symbol' ) ? get_woocommerce_currency_symbol() : '',
				'currency_pos'              => get_option( 'woocommerce_currency_pos', 'left' ),
				'price_decimals'            => function_exists( 'wc_get_price_decimals' ) ? wc_get_price_decimals() : 2,
				'price_decimal_sep'         => function_exists( 'wc_get_price_decimal_separator' ) ? wc_get_price_decimal_separator() : '.',
				'price_thousand_sep'        => function_exists( 'wc_get_price_thousand_separator' ) ? wc_get_price_thousand_separator() : ',',
				'note_max_length'           => self::CART_NOTE_MAX_LENGTH,
				'show_topping_group_labels' => $show_topping_group_labels,
				'modal_upsells_enabled'     => $modal_upsells_enabled,
				'i18n'                      => array(
					'loading_product' => __( 'Loading product...', 'foodord' ),
				),
			)
		);
	}

	/**
	 * AJAX handler for getting product modal content
	 */
	public function ajax_get_product_modal() {
		try {
			// Check if WooCommerce is active.
			if ( ! function_exists( 'wc_get_product' ) ) {
				wp_send_json_error( esc_html__( 'WooCommerce is not active', 'foodord' ) );
				return;
			}

			// Check nonce.
			if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'foodord_product_modal_nonce' ) ) {
				wp_send_json_error( esc_html__( 'Security check failed', 'foodord' ) );
				return;
			}

			// Check product ID.
			if ( ! isset( $_POST['product_id'] ) || empty( $_POST['product_id'] ) ) {
				wp_send_json_error( esc_html__( 'No product ID provided', 'foodord' ) );
				return;
			}

			// Get product ID.
			$product_id = absint( $_POST['product_id'] );
			$context    = isset( $_POST['context'] ) ? sanitize_key( wp_unslash( $_POST['context'] ) ) : '';
			$is_reservation_preorder = 'reservation_preorder' === $context;

			// Get product.
			$product = wc_get_product( $product_id );

			// Check if product exists.
			if ( ! $product ) {
				wp_send_json_error( esc_html__( 'Product not found', 'foodord' ) );
				return;
			}

			if ( $is_reservation_preorder && ( ! class_exists( 'Foodord_Table_Reservation_Preorders' ) || ! Foodord_Table_Reservation_Preorders::is_product_enabled( $product_id ) ) ) {
				wp_send_json_error( esc_html__( 'This product is not available for reservation pre-order.', 'foodord' ) );
				return;
			}

			if ( ! $is_reservation_preorder && class_exists( 'Foodord_Product_Availability' ) && ! Foodord_Product_Availability::is_available( $product_id ) ) {
				wp_send_json_error( esc_html__( 'This product is currently unavailable.', 'foodord' ) );
				return;
			}

			// Check if template file exists.
			$template_file = FOODORD_DIR . 'templates/frontend/product-modal.php';
			if ( ! file_exists( $template_file ) ) {
				wp_send_json_error( esc_html__( 'Template file not found', 'foodord' ) );
				return;
			}

			// Start output buffer.
			ob_start();

			// Temporarily convert variation dropdowns to radio in modal context only.
			add_filter( 'woocommerce_dropdown_variation_attribute_options_html', array( $this, 'foodord_modal_dropdown_to_radio' ), 10, 2 );

			// Load template.
			$foodord_topping_sets = $this->prepare_product_topping_sets( $product_id );
			$foodord_modal_upsells = array();
			$modal_upsells_enabled = $is_reservation_preorder ? 'no' : get_option( 'foodord_enable_modal_upsells', 'no' );
			$foodord_upsell_helper = $this->modal_upsells;
			$foodord_modal_context = $context;

			if ( 'yes' === $modal_upsells_enabled && $this->modal_upsells instanceof Foodord_Product_Modal_Upsells ) {
				$foodord_modal_upsells = $this->modal_upsells->get_products_for_modal( $product );
			}

			if ( $is_reservation_preorder ) {
				Foodord_Table_Reservation_Preorders::begin_pricing_rules_bypass();
			}
			try {
				include $template_file;
			} finally {
				if ( $is_reservation_preorder ) {
					Foodord_Table_Reservation_Preorders::end_pricing_rules_bypass();
				}
			}

			// Remove filter immediately after rendering to avoid global side effects.
			remove_filter( 'woocommerce_dropdown_variation_attribute_options_html', array( $this, 'foodord_modal_dropdown_to_radio' ), 10 );

			// Get buffer contents.
			$html = ob_get_clean();

			$prefill_payload = null;
			if ( isset( $_POST['cart_item_key'] ) && function_exists( 'WC' ) && WC()->cart ) {
				$requested_key = sanitize_text_field( wp_unslash( $_POST['cart_item_key'] ) );
				if ( '' !== $requested_key && isset( WC()->cart->cart_contents[ $requested_key ] ) ) {
					$cart_item       = WC()->cart->cart_contents[ $requested_key ];
					$cart_product_id = isset( $cart_item['product_id'] ) ? (int) $cart_item['product_id'] : 0;
					if ( $cart_product_id === $product_id ) {
						$prefill_payload = array(
							'cart_item_key'      => $requested_key,
							'product_id'         => $cart_product_id,
							'quantity'           => isset( $cart_item['quantity'] ) ? max( 1, (int) $cart_item['quantity'] ) : 1,
							'variation_id'       => isset( $cart_item['variation_id'] ) ? (int) $cart_item['variation_id'] : 0,
							'toppings_signature' => isset( $cart_item['foodord_toppings_signature'] ) ? (string) $cart_item['foodord_toppings_signature'] : '',
							'attributes'         => array(),
							'toppings'           => array(),
						);

						if ( ! empty( $cart_item['variation'] ) && is_array( $cart_item['variation'] ) ) {
							foreach ( $cart_item['variation'] as $attr_name => $attr_value ) {
								$sanitized_key                                   = sanitize_key( (string) $attr_name );
								$sanitized_value                                 = sanitize_text_field( (string) $attr_value );
								$prefill_payload['attributes'][ $sanitized_key ] = $sanitized_value;
							}
						}

						if ( ! empty( $cart_item['foodord_toppings'] ) && is_array( $cart_item['foodord_toppings'] ) ) {
							foreach ( $cart_item['foodord_toppings'] as $set ) {
								$set_id = isset( $set['set_id'] ) ? sanitize_text_field( (string) $set['set_id'] ) : '';
								if ( '' === $set_id ) {
									continue;
								}
								$group_type     = isset( $set['group_type'] ) ? sanitize_text_field( (string) $set['group_type'] ) : '';
								$selected_items = array();
								if ( ! empty( $set['items'] ) && is_array( $set['items'] ) ) {
									foreach ( $set['items'] as $selection ) {
										if ( isset( $selection['id'] ) ) {
											$selected_items[] = sanitize_text_field( (string) $selection['id'] );
										}
									}
								}
								$prefill_payload['toppings'][] = array(
									'set_id'     => $set_id,
									'group_type' => $group_type,
									'items'      => $selected_items,
								);
							}
						}

						if ( isset( $cart_item['foodord_note'] ) && '' !== $cart_item['foodord_note'] ) {
							$prefill_payload['note'] = sanitize_textarea_field( (string) $cart_item['foodord_note'] );
						}
					}
				}
			}

			// Send response.
			wp_send_json_success(
				array(
					'html'    => $html,
					'prefill' => $prefill_payload,
				)
			);

		} catch ( Exception $e ) {
			wp_send_json_error( 'PHP Error: ' . $e->getMessage() );
		}
	}

	/**
	 * AJAX handler for updating cart quantity
	 */
	public function ajax_update_cart_quantity() {
		// Check nonce.
		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'foodord_product_modal_nonce' ) ) {
			wp_send_json_error( 'Invalid nonce' );
		}

		// Check if WooCommerce is active.
		if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
			wp_send_json_error( 'WooCommerce not active' );
		}

		// Get and validate product ID and quantity.
		$requested_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0;
		$variation_id = isset( $_POST['variation_id'] ) ? absint( $_POST['variation_id'] ) : 0;
		$quantity     = isset( $_POST['quantity'] ) ? absint( $_POST['quantity'] ) : 1;

		if ( ! $requested_id && ! $variation_id ) {
			wp_send_json_error( 'Invalid product ID' );
		}

		// Find the cart item key.
		$cart_item_key  = null;
		$cart_item_data = null;
		foreach ( WC()->cart->get_cart() as $key => $cart_item ) {
			$cart_product_id   = isset( $cart_item['product_id'] ) ? (int) $cart_item['product_id'] : 0;
			$cart_variation_id = isset( $cart_item['variation_id'] ) ? (int) $cart_item['variation_id'] : 0;

			$matches_explicit_variation  = $variation_id && $variation_id === $cart_variation_id;
			$matches_requested_variation = $cart_variation_id && $requested_id === $cart_variation_id;
			$matches_simple_product      = 0 === $cart_variation_id && $requested_id === $cart_product_id;

			if ( $matches_explicit_variation || $matches_requested_variation || $matches_simple_product ) {
				$cart_item_key  = $key;
				$cart_item_data = $cart_item;
				break;
			}
		}

		if ( ! $cart_item_key || ! $cart_item_data ) {
			wp_send_json_error( 'Product not in cart' );
		}

		// Update cart quantity.
		WC()->cart->set_quantity( $cart_item_key, $quantity );

		// Get updated cart item data for line total calculation.
		$updated_cart_item = WC()->cart->get_cart_item( $cart_item_key );
		$item_line_total   = '';
		$item_unit_price   = '';
		if ( $updated_cart_item && isset( $updated_cart_item['data'] ) ) {
			$product = $updated_cart_item['data'];
			if ( $product instanceof WC_Product ) {
				// Calculate line total (unit price × quantity).
				$line_total      = $product->get_price() * $quantity;
				$item_line_total = wc_price( $line_total );
				$item_unit_price = wc_price( $product->get_price() );
			}
		}

		// Get updated cart fragments.
		$fragments = array();
		ob_start();
		woocommerce_mini_cart();
		$mini_cart                                     = ob_get_clean();
		$fragments['div.widget_shopping_cart_content'] = '<div class="widget_shopping_cart_content">' . $mini_cart . '</div>';

		$summary                    = $this->build_cart_summary_payload();
		$summary['fragments']       = $fragments;
		$summary['item_line_total'] = $item_line_total;
		$summary['item_unit_price'] = $item_unit_price;
		$summary['item_quantity']   = $quantity;

		wp_send_json_success( $summary );
	}

	/**
	 * AJAX handler for removing cart item
	 */
	public function ajax_remove_cart_item() {
		// Check nonce.
		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'foodord_product_modal_nonce' ) ) {
			wp_send_json_error( 'Invalid nonce' );
		}

		// Check if WooCommerce is active.
		if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
			wp_send_json_error( 'WooCommerce not active' );
		}

		// Get and validate product ID.
		$requested_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0;
		$variation_id = isset( $_POST['variation_id'] ) ? absint( $_POST['variation_id'] ) : 0;

		if ( ! $requested_id && ! $variation_id ) {
			wp_send_json_error( 'Invalid product ID' );
		}

		// Find and remove the cart item.
		$removed = false;
		foreach ( WC()->cart->get_cart() as $key => $cart_item ) {
			$cart_product_id   = isset( $cart_item['product_id'] ) ? (int) $cart_item['product_id'] : 0;
			$cart_variation_id = isset( $cart_item['variation_id'] ) ? (int) $cart_item['variation_id'] : 0;

			$matches_explicit_variation  = $variation_id && $variation_id === $cart_variation_id;
			$matches_requested_variation = $cart_variation_id && $requested_id === $cart_variation_id;
			$matches_simple_product      = 0 === $cart_variation_id && $requested_id === $cart_product_id;

			if ( $matches_explicit_variation || $matches_requested_variation || $matches_simple_product ) {
				WC()->cart->remove_cart_item( $key );
				$removed = true;
				break;
			}
		}

		if ( ! $removed ) {
			wp_send_json_error( 'Product not in cart' );
		}

		// Get updated cart fragments.
		$fragments = array();
		ob_start();
		woocommerce_mini_cart();
		$mini_cart                                     = ob_get_clean();
		$fragments['div.widget_shopping_cart_content'] = '<div class="widget_shopping_cart_content">' . $mini_cart . '</div>';

		$summary              = $this->build_cart_summary_payload();
		$summary['fragments'] = $fragments;

		wp_send_json_success( $summary );
	}

	/**
	 * AJAX handler for clearing entire cart
	 */
	public function ajax_clear_cart() {
		// Check nonce.
		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'foodord_product_modal_nonce' ) ) {
			wp_send_json_error( 'Invalid nonce' );
		}

		// Check if WooCommerce is active.
		if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
			wp_send_json_error( 'WooCommerce not active' );
		}

		// Clear the cart.
		WC()->cart->empty_cart();

		// Get updated cart fragments.
		$fragments = array();
		ob_start();
		woocommerce_mini_cart();
		$mini_cart                                     = ob_get_clean();
		$fragments['div.widget_shopping_cart_content'] = '<div class="widget_shopping_cart_content">' . $mini_cart . '</div>';

		$summary              = $this->build_cart_summary_payload();
		$summary['fragments'] = $fragments;

		wp_send_json_success( $summary );
	}

	/**
	 * Build cart summary payload for AJAX responses.
	 *
	 * @return array Cart summary data including items, totals, and delivery info.
	 */
	protected function build_cart_summary_payload() {
		$show_topping_group_labels = 'yes' === get_option( 'foodord_show_topping_group_label', 'yes' );
		$is_table_ordering         = foodord_is_table_ordering_request();
		$payload                   = array(
			'cart_items'                => array(),
			'card_upsells_html'         => '',
			'cart_count'                => 0,
			'cart_contents_count'       => 0,
			'cart_hash'                 => '',
			'cart_total_html'           => function_exists( 'wc_price' ) ? wc_price( 0 ) : '0',
			'cart_subtotal'             => 0,
			'subtotal_html'             => function_exists( 'wc_price' ) ? wc_price( 0 ) : '0',
			'delivery_fee'              => 0,
			'delivery_fee_html'         => '',
			'fees'                      => array(),
			'fees_total'                => 0,
			'fees_total_html'           => function_exists( 'wc_price' ) ? wc_price( 0 ) : '0',
			'minimum_active'            => false,
			'minimum_floor_html'        => function_exists( 'wc_price' ) ? wc_price( 0 ) : '0',
			'minimum_required'          => 0,
			'minimum_required_html'     => '',
			'minimum_passed'            => true,
			'remaining_to_minimum'      => 0,
			'remaining_to_minimum_html' => '',
			'show_topping_group_labels' => $show_topping_group_labels,
		);

		if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
			return $payload;
		}

		if ( isset( $GLOBALS['foodord_pro_pricing_runtime'] ) && method_exists( $GLOBALS['foodord_pro_pricing_runtime'], 'apply_bogo_discounts' ) ) {
			$GLOBALS['foodord_pro_pricing_runtime']->apply_bogo_discounts( WC()->cart );
		} elseif ( class_exists( 'Foodord_Pro_Pricing_Runtime' ) ) {
			// Attempt to instantiate runtime if not already booted.
			$GLOBALS['foodord_pro_pricing_runtime'] = $GLOBALS['foodord_pro_pricing_runtime'] ?? new Foodord_Pro_Pricing_Runtime();
			if ( method_exists( $GLOBALS['foodord_pro_pricing_runtime'], 'apply_bogo_discounts' ) ) {
				$GLOBALS['foodord_pro_pricing_runtime']->apply_bogo_discounts( WC()->cart );
			}
		}

		WC()->cart->calculate_fees();

		$cart_items = array();
		foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
			$product = isset( $cart_item['data'] ) && $cart_item['data'] instanceof WC_Product ? $cart_item['data'] : wc_get_product( isset( $cart_item['product_id'] ) ? (int) $cart_item['product_id'] : 0 );
			if ( ! $product ) {
				continue;
			}

			$toppings_info       = array();
			$hide_map            = array();
			$label_map           = array();
			$product_id_for_sets = isset( $cart_item['product_id'] ) ? (int) $cart_item['product_id'] : ( $product ? (int) $product->get_id() : 0 );
			if ( $product_id_for_sets > 0 ) {
				$sets_for_product = $this->prepare_product_topping_sets( $product_id_for_sets );
				if ( is_array( $sets_for_product ) ) {
					foreach ( $sets_for_product as $set ) {
						if ( isset( $set['id'] ) ) {
							$set_id_key               = (string) $set['id'];
							$hide_map[ $set_id_key ]  = ! empty( $set['hide_price'] );
							$label_map[ $set_id_key ] = isset( $set['label'] ) ? sanitize_text_field( (string) $set['label'] ) : '';
						}
					}
				}
			}

			if ( ! empty( $cart_item['foodord_toppings'] ) && is_array( $cart_item['foodord_toppings'] ) ) {
				foreach ( $cart_item['foodord_toppings'] as $set ) {
					if ( empty( $set['items'] ) || ! is_array( $set['items'] ) ) {
						continue;
					}

					$set_name            = isset( $set['set_name'] ) && '' !== $set['set_name'] ? sanitize_text_field( (string) $set['set_name'] ) : __( 'Toppings', 'foodord' );
					$set_id              = isset( $set['set_id'] ) ? (string) $set['set_id'] : '';
					$hide_prices_for_set = ( '' !== $set_id && isset( $hide_map[ $set_id ] ) ) ? (bool) $hide_map[ $set_id ] : false;
					$raw_label           = isset( $set['set_label'] ) && '' !== $set['set_label'] ? sanitize_text_field( (string) $set['set_label'] ) : '';
					if ( '' === $raw_label && '' !== $set_id && isset( $label_map[ $set_id ] ) && '' !== $label_map[ $set_id ] ) {
						$raw_label = $label_map[ $set_id ];
					}
					$display_label = '' !== $raw_label ? $raw_label : $set_name;
					$items         = array();

					foreach ( $set['items'] as $selection ) {
						$label = isset( $selection['label'] ) ? $selection['label'] : '';
						if ( '' === $label ) {
							continue;
						}
						$price = isset( $selection['price'] ) ? (float) $selection['price'] : 0.0;
						if ( ! $hide_prices_for_set && $price > 0 && function_exists( 'wc_price' ) ) {
							$raw_price  = html_entity_decode( wp_strip_all_tags( wc_price( $price ) ), ENT_QUOTES, 'UTF-8' );
							$normalized = trim( preg_replace( '/\x{00A0}/u', ' ', $raw_price ) );
							$items[]    = sprintf( '%s (+%s)', $label, $normalized );
							continue;
						}
						$items[] = $label;
					}

					if ( ! empty( $items ) ) {
						$toppings_info[] = array(
							'set_name'  => $set_name,
							'set_label' => $display_label,
							'items'     => $items,
						);
					}
				}
			}

			$unit_price = (float) $product->get_price() + $this->get_topping_tax_fee_unit_total( $cart_item );
			$line_total = $unit_price * $cart_item['quantity'];

			$cart_items[] = array(
				'id'                 => $product->get_id(),
				'name'               => $product->get_name(),
				'price'              => wc_price( $line_total ),
				'unit_price'         => wc_price( $unit_price ),
				'unit_price_numeric' => $unit_price,
				'line_total'         => wc_price( $line_total ),
				'quantity'           => $cart_item['quantity'],
				'product_id'         => isset( $cart_item['product_id'] ) ? (int) $cart_item['product_id'] : 0,
				'variation_id'       => isset( $cart_item['variation_id'] ) ? (int) $cart_item['variation_id'] : 0,
				'toppings'           => $toppings_info,
				'toppings_signature' => isset( $cart_item['foodord_toppings_signature'] ) ? (string) $cart_item['foodord_toppings_signature'] : '',
				'cart_item_key'      => (string) $cart_item_key,
				'note'               => isset( $cart_item['foodord_note'] ) ? $this->sanitize_cart_item_note( (string) $cart_item['foodord_note'] ) : '',
			);
		}

		$topping_tax_fee_total = $this->get_cart_topping_tax_fee_total( WC()->cart );
		$cart_subtotal         = WC()->cart->get_cart_contents_total() + $topping_tax_fee_total;
		if ( wc_tax_enabled() && WC()->cart->display_prices_including_tax() ) {
			$cart_subtotal += WC()->cart->get_cart_contents_tax();
		}
		$subtotal_html = function_exists( 'wc_price' ) ? wc_price( $cart_subtotal ) : number_format( $cart_subtotal, 2 );

		$mode              = $this->resolve_order_mode();
		$delivery_details  = $is_table_ordering
			? array(
				'amount' => 0.0,
				'html'   => '',
			)
			: $this->get_delivery_fee_details( $mode );
		$delivery_fee      = $delivery_details['amount'];
		$delivery_fee_html = $delivery_details['html'];

		$fees       = array();
		$fees_total = 0.0;
		$cart_fees  = WC()->cart->get_fees();
		if ( ! empty( $cart_fees ) ) {
			foreach ( $cart_fees as $fee_obj ) {
				if ( ! empty( $fee_obj->foodord_topping_tax_fee ) ) {
					continue;
				}
				if ( ! isset( $fee_obj->amount ) ) {
					continue;
				}
				$fee_amount = (float) $fee_obj->amount;
				if ( $fee_amount >= 0 ) {
					continue;
				}
				$fees_total += $fee_amount;
				$fees[]      = array(
					'label'       => isset( $fee_obj->name ) ? wp_strip_all_tags( (string) $fee_obj->name ) : __( 'Fee', 'foodord' ),
					'amount'      => $fee_amount,
					'amount_html' => function_exists( 'wc_price' ) ? wc_price( $fee_amount ) : number_format( $fee_amount, 2 ),
				);
			}
		}

		$effective_subtotal = max( 0.0, $cart_subtotal + $fees_total );
		$min_rule           = $is_table_ordering
			? array(
				'amount' => 0.0,
				'html'   => '',
				'active' => false,
			)
			: $this->get_minimum_order_details( $mode );
		$minimum_required   = $min_rule['amount'];
		$minimum_html       = $min_rule['html'];
		$minimum_active     = $min_rule['active'];
		$minimum_passed     = ! $minimum_active || ( $effective_subtotal >= $minimum_required );
		$remaining_to_min   = max( 0.0, $minimum_required - $effective_subtotal );
		$remaining_html     = $remaining_to_min > 0 ? ( function_exists( 'wc_price' ) ? wc_price( $remaining_to_min ) : number_format( $remaining_to_min, 2 ) ) : '';

		$cart_count          = WC()->cart->get_cart_contents_count();
		$total_with_delivery = $effective_subtotal + ( ( $cart_count > 0 && $minimum_passed ) ? $delivery_fee : 0 );
		$total_html          = function_exists( 'wc_price' ) ? wc_price( $total_with_delivery ) : number_format( $total_with_delivery, 2 );

		$payload['cart_items']                = $cart_items;
		$payload['card_upsells_html']         = $this->card_upsells ? $this->card_upsells->get_markup_for_cart() : '';
		$payload['cart_count']                = $cart_count;
		$payload['cart_contents_count']       = $cart_count;
		$payload['cart_hash']                 = WC()->cart->get_cart_hash();
		$payload['cart_total_html']           = $total_html;
		$payload['cart_subtotal']             = $cart_subtotal;
		$payload['subtotal_html']             = $subtotal_html;
		$payload['delivery_fee']              = $delivery_fee;
		$payload['delivery_fee_html']         = $delivery_fee_html;
		$payload['fees']                      = $fees;
		$payload['fees_total']                = $fees_total;
		$payload['fees_total_html']           = function_exists( 'wc_price' ) ? wc_price( $fees_total ) : number_format( $fees_total, 2 );
		$payload['minimum_active']            = (bool) $minimum_active;
		$payload['minimum_required']          = (float) $minimum_required;
		$payload['minimum_required_html']     = $minimum_html;
		$payload['minimum_passed']            = (bool) $minimum_passed;
		$payload['remaining_to_minimum']      = (float) $remaining_to_min;
		$payload['remaining_to_minimum_html'] = $remaining_html;
		$payload['show_topping_group_labels'] = $show_topping_group_labels;

		return $payload;
	}

	/**
	 * AJAX handler for getting current cart items
	 */
	public function ajax_get_cart_items() {
		// Check nonce.
		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'foodord_product_modal_nonce' ) ) {
			wp_send_json_error( 'Invalid nonce' );
		}

		// Check if WooCommerce is active.
		if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
			wp_send_json_error( 'WooCommerce not active' );
		}

		// Ensure totals reflect the latest order mode and pricing rules.
		WC()->cart->calculate_totals();

		wp_send_json_success( $this->build_cart_summary_payload() );
	}

	/**
	 * AJAX handler for updating cart item note.
	 */
	public function ajax_update_cart_item_note() {
		// Nonce check recognized by pre-commit heuristic.
		check_admin_referer( 'foodord_product_modal_nonce', 'nonce' );

		if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
			wp_send_json_error( 'WooCommerce not active', 400 );
		}

		$cart_item_key = isset( $_POST['cart_item_key'] ) ? sanitize_text_field( wp_unslash( $_POST['cart_item_key'] ) ) : '';
		if ( '' === $cart_item_key ) {
			wp_send_json_error( 'Missing cart item key', 400 );
		}

		$cart = WC()->cart;
		if ( ! isset( $cart->cart_contents[ $cart_item_key ] ) ) {
			wp_send_json_error( 'Cart item not found', 404 );
		}

		$raw_note = isset( $_POST['note'] ) ? sanitize_text_field( wp_unslash( $_POST['note'] ) ) : '';
		$note     = $this->sanitize_cart_item_note( $raw_note );

		if ( '' === $note ) {
			unset( $cart->cart_contents[ $cart_item_key ]['foodord_note'] );
		} else {
			$cart->cart_contents[ $cart_item_key ]['foodord_note'] = $note;
		}

		$cart->set_session();
		$cart->maybe_set_cart_cookies();

		wp_send_json_success(
			array(
				'cart_item_key' => $cart_item_key,
				'note'          => $note,
				'note_html'     => esc_html( $note ),
			)
		);
	}

	/**
	 * Extract option value/label pairs from a variation <select> HTML string.
	 *
	 * @param string $select_html Variation select markup.
	 * @return array<int, array<string, string>>
	 */
	private function extract_variation_choices_from_select_html( $select_html ) {
		if ( ! is_string( $select_html ) || '' === $select_html ) {
			return array();
		}

		$choices = array();

		if ( preg_match_all( '/<option[^>]*value=(["\'])(.*?)\1[^>]*>(.*?)<\/option>/is', $select_html, $matches, PREG_SET_ORDER ) ) {
			foreach ( $matches as $match ) {
				$value = isset( $match[2] ) ? (string) $match[2] : '';
				if ( '' === $value ) {
					continue;
				}

				$label_html = isset( $match[3] ) ? (string) $match[3] : '';
				$label      = trim( wp_strip_all_tags( wp_specialchars_decode( $label_html, ENT_QUOTES ) ) );

				if ( '' === $label ) {
					$label = $value;
				}

				$choices[] = array(
					'value' => $value,
					'label' => $label,
				);
			}
		}

		return $choices;
	}

	/**
	 * Filter WooCommerce variation dropdown HTML to render radios for modal context.
	 *
	 * We keep the original <select> in the DOM (hidden) so WooCommerce's JS logic
	 * continues to work. Radios are synced with the select via JS.
	 *
	 * @param string $html Original select HTML.
	 * @param array  $args Arguments passed by wc_dropdown_variation_attribute_options.
	 * @return string Modified HTML containing hidden select + radio group.
	 */
	public function foodord_modal_dropdown_to_radio( $html, $args ) {
		// Defensive checks.
		if ( empty( $args ) || empty( $args['attribute'] ) || empty( $args['product'] ) || ! $args['product'] instanceof \WC_Product ) {
			return $html;
		}

		$attribute = $args['attribute'];
		$product   = $args['product'];
		$options   = $args['options'];
		$selected  = isset( $args['selected'] ) ? (string) $args['selected'] : '';
		$id        = $args['id'] ? $args['id'] : sanitize_title( $attribute );
		$name      = $args['name'] ? $args['name'] : 'attribute_' . sanitize_title( $attribute );

		// Build a normalized list of [value, label].
		$choices = $this->extract_variation_choices_from_select_html( $html );
		if ( empty( $options ) && ! empty( $product ) && ! empty( $attribute ) ) {
			$attributes = $product->get_variation_attributes();
			$options    = isset( $attributes[ $attribute ] ) ? $attributes[ $attribute ] : array();
		}

		if ( empty( $choices ) && ! empty( $options ) ) {
			if ( taxonomy_exists( $attribute ) ) {
				// Keep the order as defined in $options array (which comes from product variation attributes).
				$terms = wc_get_product_terms( $product->get_id(), $attribute, array( 'fields' => 'all' ) );

				// Create term lookup array.
				$term_lookup = array();
				foreach ( $terms as $term ) {
					$term_lookup[ $term->slug ] = $term;
				}
				// Build choices in the same order as $options.
				foreach ( $options as $option_slug ) {
					if ( isset( $term_lookup[ $option_slug ] ) ) {
						$term      = $term_lookup[ $option_slug ];
						$choices[] = array(
							'value' => (string) $term->slug,
							'label' => (string) $term->name,
						);
					}
				}
			} else {
				// Custom attributes - already in correct order.
				foreach ( $options as $option ) {
					$choices[] = array(
						'value' => (string) $option,
						'label' => (string) $option,
					);
				}
			}
		}

		// Build radio group markup. Keep original select hidden (scoped with CSS).
		$attr_label    = wc_attribute_label( $attribute );
		$radio_name    = 'foodord_radio_' . sanitize_title( $name );
		$radio_id_base = 'foodord-' . sanitize_title( $id ) . '-';

		// Check if this attribute is required by checking the original HTML for 'required' attribute.
		// Also check if this is a required variation attribute by WooCommerce standards.
		$is_required = strpos( $html, 'required' ) !== false ||
						( isset( $args['required'] ) && $args['required'] ) ||
						( $product && $product->get_type() === 'variable' && ! empty( $options ) );

		// Get available variations for price display.
		$variations       = $product->get_available_variations();
		$variation_prices = array();

		// Map variation attributes to their prices.
		foreach ( $variations as $variation ) {
			if ( isset( $variation['attributes'] ) && isset( $variation['display_price'] ) ) {
				$attr_key = 'attribute_' . sanitize_title( $attribute );
				if ( isset( $variation['attributes'][ $attr_key ] ) ) {
					$attr_value                      = $variation['attributes'][ $attr_key ];
					$variation_prices[ $attr_value ] = wc_price( wc_get_price_to_display( $product, array( 'price' => $variation['display_price'] ) ) );
				}
			}
		}

		// Build header outside of the container.
		$header_html  = '<div class="foodord-attr-legend">';
		$header_html .= '<span class="foodord-attr-legend-title">' . esc_html( $attr_label ) . '</span>';
		if ( $is_required ) {
			$header_html .= '<span class="foodord-product-toppings__badge foodord-product-toppings__badge--required" data-required-text="' . esc_attr__( 'Required', 'foodord' ) . '">' . esc_html__( 'Required', 'foodord' ) . '</span>';
		} else {
			$header_html .= '<span class="foodord-product-toppings__badge foodord-product-toppings__badge--optional">' . esc_html__( 'Optional', 'foodord' ) . '</span>';
		}
		$header_html .= '</div>';

		$radios_html  = '<div class="foodord-attr-radio" data-attribute="' . esc_attr( $attribute ) . '">';
		$radios_html .= '<div class="foodord-attr-options">';

		foreach ( $choices as $idx => $choice ) {
			$val          = (string) $choice['value'];
			$label        = (string) $choice['label'];
			$rid          = $radio_id_base . (string) $idx . '-' . sanitize_title( $val );
			$checked      = ( sanitize_title( $selected ) === sanitize_title( $val ) ) ? ' checked' : '';
			$radios_html .= '<label class="foodord-attr-option" for="' . esc_attr( $rid ) . '">';
			$radios_html .= '<div class="foodord-attr-option-left">';
			$radios_html .= '<input type="radio" class="foodord-attr-input" id="' . esc_attr( $rid ) . '" name="' . esc_attr( $radio_name ) . '" value="' . esc_attr( $val ) . '" data-target-select="#' . esc_attr( $id ) . '"' . $checked . ' />';
			$radios_html .= '<span class="foodord-attr-text">' . esc_html( $label ) . '</span>';
			$radios_html .= '</div>';

			// Add price if available for this variation.
			if ( isset( $variation_prices[ $val ] ) ) {
				$radios_html .= '<span class="foodord-attr-price">' . $variation_prices[ $val ] . '</span>';
			}
			$radios_html .= '</label>';
		}

		$radios_html .= '</div>'; // .foodord-attr-options.
		$radios_html .= '</div>'; // .foodord-attr-radio.

		$required_attr   = $is_required ? ' data-is-required="1"' : '';
		$group_type_attr = ' data-group-type="radio"';

		return $header_html . '<fieldset class="foodord-attr-wrapper"' . $required_attr . $group_type_attr . '><div class="foodord-original-select" aria-hidden="true">' . $html . '</div>' . $radios_html . '</fieldset>';
	}

	/**
	 * AJAX handler for adding products to cart
	 */
	public function ajax_add_to_cart() {
		// Check if WooCommerce is active.
		if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
			wp_send_json_error( 'WooCommerce is not active' );
			return;
		}

		// Check nonce.
		if ( ! isset( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['nonce'] ) ), 'foodord_product_modal_nonce' ) ) {
			wp_send_json_error( 'Security check failed' );
			return;
		}

		// Get product ID and quantity.
		$product_id            = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0;
		$quantity              = isset( $_POST['quantity'] ) ? absint( $_POST['quantity'] ) : 1;
		$variation_id          = isset( $_POST['variation_id'] ) ? absint( $_POST['variation_id'] ) : 0;
		$cart_item_key_request = isset( $_POST['cart_item_key'] ) ? sanitize_text_field( wp_unslash( $_POST['cart_item_key'] ) ) : '';
		$provided_signature    = isset( $_POST['foodord_toppings_signature'] ) ? sanitize_text_field( wp_unslash( $_POST['foodord_toppings_signature'] ) ) : '';
		$editing_mode          = isset( $_POST['foodord_editing'] ) && '1' === sanitize_text_field( wp_unslash( $_POST['foodord_editing'] ) );
		$original_signature    = isset( $_POST['foodord_original_signature'] ) ? sanitize_text_field( wp_unslash( $_POST['foodord_original_signature'] ) ) : '';

		if ( ! $product_id ) {
			wp_send_json_error( 'Invalid product ID' );
			return;
		}

		if ( class_exists( 'Foodord_Product_Availability' ) ) {
			$availability_ids = array( $product_id );
			if ( $variation_id > 0 ) {
				$availability_ids[] = $variation_id;
			}
			foreach ( $availability_ids as $availability_id ) {
				if ( $availability_id > 0 && ! Foodord_Product_Availability::is_available( $availability_id ) ) {
					wp_send_json(
						array(
							'error'         => true,
							'error_message' => esc_html__( 'This product is currently unavailable.', 'foodord' ),
						)
					);
					return;
				}
			}
		}

		$cart_item_data = array();
		$raw_toppings   = isset( $_POST['foodord_toppings'] ) ? $this->sanitize_toppings_request_payload( map_deep( wp_unslash( $_POST['foodord_toppings'] ), 'sanitize_text_field' ) ) : array();

		$required_validation = $this->validate_required_toppings( $product_id, $raw_toppings );
		if ( is_wp_error( $required_validation ) ) {
			wp_send_json(
				array(
					'error'         => true,
					'error_message' => $required_validation->get_error_message(),
					'missing_sets'  => $required_validation->get_error_data(),
				)
			);
			return;
		}

		$parsed_toppings = $this->build_cart_item_toppings_payload( $product_id, $raw_toppings );
		if ( ! empty( $parsed_toppings ) ) {
			$cart_item_data['foodord_toppings']           = $parsed_toppings['selections'];
			$cart_item_data['foodord_toppings_signature'] = $parsed_toppings['signature'];
			$cart_item_data['foodord_toppings_extra']     = $parsed_toppings['extra'];
			$cart_item_data['foodord_topping_tax_fees']   = $parsed_toppings['tax_fees'];
		}

		$product_for_base = null;
		if ( $variation_id > 0 ) {
			$product_for_base = wc_get_product( $variation_id );
		} else {
			$product_for_base = wc_get_product( $product_id );
		}
		if ( $product_for_base ) {
			$cart_item_data['foodord_toppings_base_price'] = (float) $product_for_base->get_price( 'edit' );
		}

		$cart = WC()->cart;
		if ( $cart_item_key_request && isset( $cart->cart_contents[ $cart_item_key_request ] ) ) {
			$existing_item       = $cart->cart_contents[ $cart_item_key_request ];
			$existing_product_id = isset( $existing_item['product_id'] ) ? (int) $existing_item['product_id'] : $product_id;
			$existing_variation  = isset( $existing_item['variation_id'] ) ? (int) $existing_item['variation_id'] : 0;
			$existing_signature  = isset( $existing_item['foodord_toppings_signature'] ) ? (string) $existing_item['foodord_toppings_signature'] : '';

			$target_variation = $variation_id > 0 ? $variation_id : $existing_variation;

			if ( $existing_product_id === $product_id && $existing_variation === $target_variation && $existing_signature === $provided_signature ) {
				if ( ! empty( $cart_item_data ) ) {
					$cart->cart_contents[ $cart_item_key_request ] = array_merge( $cart->cart_contents[ $cart_item_key_request ], $cart_item_data );
				}
				$cart->set_quantity( $cart_item_key_request, $quantity, true );
				$cart->set_session();
				$cart->maybe_set_cart_cookies();
				WC_AJAX::get_refreshed_fragments();
				return;
			}

			if ( $editing_mode && $existing_product_id === $product_id ) {
				$original_signature_matches = ( '' !== $original_signature ) && ( $existing_signature === $original_signature );
				$provided_signature_matches = ( $existing_signature === $provided_signature );
				$variation_matches          = ( $existing_variation === $target_variation );
				if ( ! ( $original_signature_matches && $provided_signature_matches && $variation_matches ) ) {
					$cart->remove_cart_item( $cart_item_key_request );
					$cart->set_session();
					$cart->maybe_set_cart_cookies();
					// Fall through to add new configuration.
				}
			}
		}

		// Handle variable product.
		if ( $variation_id > 0 ) {
			$variation = array();
			// Collect variation attributes.
			foreach ( $_POST as $key => $value ) {
				if ( strpos( $key, 'attribute_' ) === 0 ) {
					$variation[ sanitize_text_field( $key ) ] = sanitize_text_field( wp_unslash( $value ) );
				}
			}

			// Add variation to cart.
			$cart_item_key = WC()->cart->add_to_cart( $product_id, $quantity, $variation_id, $variation, $cart_item_data );
		} else {
			// Add simple product to cart.
			$cart_item_key = WC()->cart->add_to_cart( $product_id, $quantity, 0, array(), $cart_item_data );
		}

		if ( $cart_item_key ) {
			// Get cart fragments.
			WC_AJAX::get_refreshed_fragments();
		} else {
			// Get last error from WooCommerce.
			$notices       = wc_get_notices( 'error' );
			$error_message = ! empty( $notices ) ? wp_strip_all_tags( $notices[0]['notice'] ) : 'Unable to add product to cart';
			wc_clear_notices();

			wp_send_json(
				array(
					'error'         => true,
					'error_message' => $error_message,
				)
			);
		}
	}

	/**
	 * Ensure required topping groups are completed before add to cart.
	 *
	 * @param int   $product_id  Product ID.
	 * @param mixed $raw_payload Raw toppings payload from request.
	 * @return true|WP_Error
	 */
	protected function validate_required_toppings( $product_id, $raw_payload ) {
		$raw_payload = is_array( $raw_payload ) ? $raw_payload : array();
		$sets        = $this->prepare_product_topping_sets( $product_id );
		if ( empty( $sets ) ) {
			return true;
		}

		$missing_sets = array();
		foreach ( $sets as $set ) {
			$set_id = isset( $set['id'] ) ? $set['id'] : '';
			if ( '' === $set_id ) {
				continue;
			}

			$group_type   = isset( $set['group_type'] ) ? $set['group_type'] : 'dropdown';
			$is_required  = ! empty( $set['is_required'] );
			$checkbox_min = isset( $set['checkbox_min'] ) ? (int) $set['checkbox_min'] : null;
			$set_label    = isset( $set['label'] ) && '' !== $set['label'] ? $set['label'] : '';
			$set_name     = isset( $set['name'] ) && '' !== $set['name'] ? $set['name'] : '';
			$display_name = '' !== $set_label ? $set_label : $set_name;
			if ( '' === $display_name ) {
				$display_name = '';
			}

			$min_required = 0;
			if ( 'checkbox' === $group_type ) {
				if ( null !== $checkbox_min && $checkbox_min > 0 ) {
					$min_required = $checkbox_min;
				} elseif ( $is_required ) {
					$min_required = 1;
				}
			} elseif ( $is_required ) {
				$min_required = 1;
			}

			if ( $min_required <= 0 ) {
				continue;
			}

			$selected_value = isset( $raw_payload[ $set_id ] ) ? $raw_payload[ $set_id ] : null;
			$selected_count = 0;

			if ( 'checkbox' === $group_type ) {
				$selected_values = is_array( $selected_value ) ? $selected_value : array();
				$selected_values = array_filter(
					array_map(
						static function ( $value ) {
							return sanitize_text_field( (string) $value );
						},
						$selected_values
					),
					static function ( $value ) {
						return '' !== $value;
					}
				);
				$selected_count  = count( $selected_values );
			} else {
				$selected_value = is_array( $selected_value ) ? '' : sanitize_text_field( (string) $selected_value );
				if ( '' !== $selected_value ) {
					$selected_count = 1;
				}
			}

			if ( $selected_count < $min_required ) {
				$missing_sets[] = array(
					'id'   => $set_id,
					'name' => $display_name,
					'min'  => $min_required,
				);
			}
		}

		if ( empty( $missing_sets ) ) {
			return true;
		}

		$set_names = array_filter( wp_list_pluck( $missing_sets, 'name' ) );
		$cleaned   = array_map( 'sanitize_text_field', $set_names );

		if ( empty( $cleaned ) ) {
			$message = count( $missing_sets ) > 1
				? esc_html__( 'Please complete all required selections.', 'foodord' )
				: esc_html__( 'Please complete the required selection.', 'foodord' );
		} else {
			/* translators: %s: comma-separated list of topping set names */
			$message_template = _n(
				'Please complete the required selection: %s.',
				'Please complete the required selections: %s.',
				count( $cleaned ),
				'foodord'
			);
			$message          = sprintf( $message_template, esc_html( implode( ', ', $cleaned ) ) );
		}

		return new WP_Error(
			'foodord_required_toppings_missing',
			$message,
			wp_list_pluck( $missing_sets, 'id' )
		);
	}

	/**
	 * Expose prepared topping sets data for external consumers (e.g. POS REST endpoints).
	 *
	 * @param int $product_id Product ID.
	 * @return array
	 */
	public function get_product_topping_sets_for_api( $product_id ) {
		return $this->prepare_product_topping_sets( $product_id );
	}

	/**
	 * Prepare topping sets data for the product modal.
	 *
	 * @param int $product_id Product ID.
	 * @return array
	 */
	protected function prepare_product_topping_sets( $product_id ) {
		$product_id = absint( $product_id );
		if ( $product_id <= 0 ) {
			return array();
		}

		$selected_sets = get_post_meta( $product_id, '_foodord_topping_sets', true );
		$selected_sets = is_array( $selected_sets ) ? array_values( array_unique( array_map( 'sanitize_text_field', $selected_sets ) ) ) : array();

		$excluded_sets = get_post_meta( $product_id, '_foodord_topping_sets_excluded', true );
		$excluded_sets = is_array( $excluded_sets ) ? array_values( array_unique( array_map( 'sanitize_text_field', $excluded_sets ) ) ) : array();

		$saved_order = get_post_meta( $product_id, '_foodord_topping_sets_order', true );
		$saved_order = is_array( $saved_order ) ? array_values( array_unique( array_map( 'sanitize_text_field', $saved_order ) ) ) : array();

		$available_sets = get_option( 'foodord_topping_sets', array() );
		if ( empty( $available_sets ) || ! is_array( $available_sets ) ) {
			return array();
		}

		// Build automatic selection from product categories (global mappings).
		$product_term_ids = wp_get_post_terms( $product_id, 'product_cat', array( 'fields' => 'ids' ) );
		$product_term_ids = is_array( $product_term_ids ) ? array_map( 'absint', $product_term_ids ) : array();
		$mapped_set_ids   = array();
		if ( ! empty( $product_term_ids ) ) {
			foreach ( $available_sets as $set ) {
				if ( ! is_array( $set ) ) {
					continue;
				}
				$set_id = isset( $set['id'] ) ? sanitize_text_field( (string) $set['id'] ) : '';
				$cats   = isset( $set['categories'] ) && is_array( $set['categories'] ) ? array_map( 'absint', $set['categories'] ) : array();
				if ( '' === $set_id || empty( $cats ) ) {
					continue;
				}
				if ( array_intersect( $cats, $product_term_ids ) ) {
					$mapped_set_ids[] = $set_id;
				}
			}
			$mapped_set_ids = array_values( array_unique( $mapped_set_ids ) );
		}

		if ( ! empty( $excluded_sets ) && ! empty( $mapped_set_ids ) ) {
			$mapped_set_ids = array_values( array_diff( $mapped_set_ids, $excluded_sets ) );
		}

		// Union of explicitly selected sets and globally mapped sets.
		$selected_sets = empty( $selected_sets ) ? $mapped_set_ids : array_values( array_unique( array_merge( $selected_sets, $mapped_set_ids ) ) );
		if ( empty( $selected_sets ) ) {
			return array();
		}

		$indexed_sets = array();
		foreach ( $available_sets as $set ) {
			if ( ! is_array( $set ) ) {
				continue;
			}

			$set_id = isset( $set['id'] ) ? sanitize_text_field( (string) $set['id'] ) : '';
			if ( '' === $set_id || ! in_array( $set_id, $selected_sets, true ) ) {
				continue;
			}

			$group_type     = isset( $set['group_type'] ) ? sanitize_key( $set['group_type'] ) : 'dropdown';
			$allowed_types  = array( 'dropdown', 'radio', 'checkbox' );
			$group_type     = in_array( $group_type, $allowed_types, true ) ? $group_type : 'dropdown';
			$checkbox_min   = isset( $set['checkbox_min'] ) ? absint( $set['checkbox_min'] ) : null;
			$checkbox_max   = isset( $set['checkbox_max'] ) ? absint( $set['checkbox_max'] ) : null;
			$prepared_items = $this->prepare_topping_items( isset( $set['items'] ) ? $set['items'] : array(), $set_id );
			if ( empty( $prepared_items ) ) {
				continue;
			}

			$indexed_sets[ $set_id ] = array(
				'id'           => $set_id,
				'name'         => isset( $set['name'] ) ? sanitize_text_field( (string) $set['name'] ) : '',
				'label'        => isset( $set['label'] ) ? sanitize_text_field( (string) $set['label'] ) : '',
				'group_type'   => $group_type,
				'hide_price'   => ! empty( $set['hide_price'] ) ? 1 : 0,
				'is_required'  => ! empty( $set['is_required'] ) ? 1 : 0,
				'checkbox_min' => ( 'checkbox' === $group_type && null !== $checkbox_min ) ? $checkbox_min : null,
				'checkbox_max' => ( 'checkbox' === $group_type && null !== $checkbox_max ) ? $checkbox_max : null,
				'items'        => $prepared_items,
			);
		}

		if ( empty( $indexed_sets ) ) {
			return array();
		}

		$ordered_ids = array();
		foreach ( $saved_order as $ordered_id ) {
			if ( isset( $indexed_sets[ $ordered_id ] ) ) {
				$ordered_ids[] = $ordered_id;
			}
		}
		foreach ( $selected_sets as $selected_id ) {
			if ( isset( $indexed_sets[ $selected_id ] ) && ! in_array( $selected_id, $ordered_ids, true ) ) {
				$ordered_ids[] = $selected_id;
			}
		}

		$result = array();
		foreach ( $ordered_ids as $id ) {
			$result[] = $indexed_sets[ $id ];
		}

		return $result;
	}

	/**
	 * Prepare items for a single topping set.
	 *
	 * @param array  $items  Raw items.
	 * @param string $set_id Set identifier.
	 * @return array
	 */
	protected function prepare_topping_items( $items, $set_id ) {
		if ( ! is_array( $items ) ) {
			return array();
		}

		$prepared = array();
		foreach ( $items as $index => $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}

			$label = isset( $item['label'] ) ? sanitize_text_field( (string) $item['label'] ) : '';
			if ( '' === $label ) {
				continue;
			}

			$price      = isset( $item['price'] ) ? (float) $item['price'] : 0.0;
			$tax_class  = function_exists( 'wc_tax_enabled' ) && wc_tax_enabled()
				? $this->sanitize_topping_tax_class( $item['tax_class'] ?? '' )
				: '';
			$prepared[] = array(
				'id'         => $set_id . '_item_' . absint( $index ),
				'label'      => $label,
				'price'      => $price,
				'tax_class'  => $tax_class,
				'is_default' => ! empty( $item['default'] ) ? 1 : 0,
			);
		}

		return $prepared;
	}

	/**
	 * Filter WooCommerce cart item data to inject topping selections for standard flow.
	 *
	 * @param array $cart_item_data Existing cart item data.
	 * @param int   $product_id     Product ID.
	 * @param int   $variation_id   Variation ID.
	 * @return array
	 */
	public function filter_add_cart_item_data( $cart_item_data, $product_id, $variation_id ) {
		if ( isset( $cart_item_data['foodord_toppings'] ) ) {
			return $cart_item_data;
		}

		if ( empty( $_POST['foodord_toppings'] ) ) {
			return $cart_item_data;
		}

		$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
		if ( '' === $nonce || ! wp_verify_nonce( $nonce, 'foodord_product_modal_nonce' ) ) {
			return $cart_item_data;
		}

		$raw = $this->sanitize_toppings_request_payload( map_deep( wp_unslash( $_POST['foodord_toppings'] ), 'sanitize_text_field' ) );

		$parsed = $this->build_cart_item_toppings_payload( $product_id, $raw );
		if ( empty( $parsed ) ) {
			return $cart_item_data;
		}

		$cart_item_data['foodord_toppings']           = $parsed['selections'];
		$cart_item_data['foodord_toppings_signature'] = $parsed['signature'];
		$cart_item_data['foodord_toppings_extra']     = $parsed['extra'];
		$cart_item_data['foodord_topping_tax_fees']   = $parsed['tax_fees'];

		$product_for_base = $variation_id ? wc_get_product( $variation_id ) : wc_get_product( $product_id );
		if ( $product_for_base ) {
			$cart_item_data['foodord_toppings_base_price'] = (float) $product_for_base->get_price( 'edit' );
		}

		return $cart_item_data;
	}

	/**
	 * Restore topping information from WooCommerce session.
	 *
	 * @param array  $cart_item Cart item being restored.
	 * @param array  $values    Stored values.
	 * @param string $key      Cart item key.
	 * @return array
	 */
	public function restore_cart_item_from_session( $cart_item, $values, $key ) {
		if ( isset( $values['foodord_toppings'] ) ) {
			$cart_item['foodord_toppings'] = $values['foodord_toppings'];
		}
		if ( isset( $values['foodord_toppings_signature'] ) ) {
			$cart_item['foodord_toppings_signature'] = $values['foodord_toppings_signature'];
		}
		if ( isset( $values['foodord_toppings_extra'] ) ) {
			$cart_item['foodord_toppings_extra'] = $values['foodord_toppings_extra'];
		}
		if ( isset( $values['foodord_topping_tax_fees'] ) && is_array( $values['foodord_topping_tax_fees'] ) ) {
			$cart_item['foodord_topping_tax_fees'] = $values['foodord_topping_tax_fees'];
			if ( ! wc_tax_enabled() ) {
				$cart_item['foodord_toppings_extra']   = max( 0.0, (float) ( $cart_item['foodord_toppings_extra'] ?? 0 ) ) + $this->get_topping_tax_fee_unit_total( $values );
				$cart_item['foodord_topping_tax_fees'] = array();
			}
		}
		if ( isset( $values['foodord_toppings_base_price'] ) ) {
			$cart_item['foodord_toppings_base_price'] = $values['foodord_toppings_base_price'];
		}
		if ( isset( $values['foodord_note'] ) ) {
			$cart_item['foodord_note'] = $this->sanitize_cart_item_note( (string) $values['foodord_note'] );
		}

		unset( $key );
		return $this->apply_toppings_price( $cart_item );
	}

	/**
	 * Build a normalized payload for cart item toppings.
	 *
	 * @param int   $product_id Product ID.
	 * @param array $raw_payload Raw payload from the request.
	 * @return array
	 */
	protected function build_cart_item_toppings_payload( $product_id, $raw_payload ) {
		$raw_payload = is_array( $raw_payload ) ? $raw_payload : array();
		$sets        = $this->prepare_product_topping_sets( $product_id );
		if ( empty( $sets ) ) {
			return array();
		}

		$selections  = array();
		$tax_fees    = array();
		$total_extra = 0.0;
		$set_lookup  = array();
		foreach ( $sets as $set ) {
			$set_lookup[ $set['id'] ] = $set;
		}

		foreach ( $set_lookup as $set_id => $set ) {
			if ( ! isset( $raw_payload[ $set_id ] ) ) {
				continue;
			}

			$raw_value = $raw_payload[ $set_id ];
			if ( 'checkbox' === $set['group_type'] ) {
				$raw_value = is_array( $raw_value ) ? $raw_value : array( $raw_value );
			} else {
				$raw_value = array( $raw_value );
			}

			$item_lookup = array();
			foreach ( $set['items'] as $item ) {
				$item_lookup[ $item['id'] ] = $item;
			}

			$selected_items = array();
			foreach ( $raw_value as $value ) {
				$value = sanitize_text_field( (string) $value );
				if ( '' === $value || ! isset( $item_lookup[ $value ] ) ) {
					continue;
				}
				$item             = $item_lookup[ $value ];
				$price            = isset( $item['price'] ) ? (float) $item['price'] : 0.0;
				$tax_class        = $this->sanitize_topping_tax_class( $item['tax_class'] ?? '' );
				$selected_items[] = array(
					'id'    => $value,
					'label' => isset( $item['label'] ) ? $item['label'] : '',
					'price' => $price,
				);
				if ( '' === $tax_class ) {
					$total_extra += $price;
				} else {
					$tax_fees[] = array(
						'id'        => $value,
						'label'     => isset( $item['label'] ) ? sanitize_text_field( (string) $item['label'] ) : '',
						'price'     => max( 0.0, $price ),
						'tax_class' => $tax_class,
					);
				}
			}

			if ( empty( $selected_items ) ) {
				continue;
			}

			$selections[] = array(
				'set_id'     => $set['id'],
				'set_name'   => $set['name'],
				'set_label'  => isset( $set['label'] ) ? $set['label'] : '',
				'group_type' => $set['group_type'],
				'items'      => $selected_items,
			);
		}

		if ( empty( $selections ) ) {
			return array();
		}

		return array(
			'selections' => $selections,
			'extra'      => $total_extra,
			'tax_fees'   => $tax_fees,
			'signature'  => md5( wp_json_encode( $selections ) ),
		);
	}

	/**
	 * Validate and normalize a product configuration for an external order flow.
	 *
	 * @param int                 $product_id   Parent product ID.
	 * @param int                 $variation_id Variation ID.
	 * @param int                 $quantity     Requested quantity.
	 * @param array<string,mixed> $raw_toppings Requested topping selections.
	 * @param string              $note         Item note.
	 * @return array<string,mixed>|WP_Error
	 */
	public function normalize_external_order_item( int $product_id, int $variation_id, int $quantity, array $raw_toppings, string $note = '' ) {
		$product_id   = absint( $product_id );
		$variation_id = absint( $variation_id );
		$quantity     = max( 1, min( 99, absint( $quantity ) ) );
		$parent       = function_exists( 'wc_get_product' ) ? wc_get_product( $product_id ) : false;

		if ( ! $parent instanceof WC_Product || ! $parent->is_visible() ) {
			return new WP_Error( 'foodord_external_order_product_invalid', __( 'This product is no longer available.', 'foodord' ) );
		}

		$product = $parent;
		if ( $parent->is_type( 'variable' ) ) {
			$product = $variation_id > 0 ? wc_get_product( $variation_id ) : false;
			if ( ! $product instanceof WC_Product_Variation || (int) $product->get_parent_id() !== $product_id ) {
				return new WP_Error( 'foodord_external_order_variation_invalid', __( 'Please choose product options.', 'foodord' ) );
			}
		} elseif ( $variation_id > 0 ) {
			return new WP_Error( 'foodord_external_order_variation_invalid', __( 'Please choose product options.', 'foodord' ) );
		}

		if ( '' === (string) $product->get_price( 'edit' ) || ! $product->is_in_stock() || ! $product->has_enough_stock( $quantity ) ) {
			return new WP_Error( 'foodord_external_order_product_unavailable', __( 'This product is currently unavailable.', 'foodord' ) );
		}

		if ( $product->is_sold_individually() ) {
			$quantity = 1;
		}

		$required_validation = $this->validate_required_toppings( $product_id, $raw_toppings );
		if ( is_wp_error( $required_validation ) ) {
			return $required_validation;
		}

		$parsed_toppings   = $this->build_cart_item_toppings_payload( $product_id, $raw_toppings );
		$toppings          = ! empty( $parsed_toppings['selections'] ) && is_array( $parsed_toppings['selections'] ) ? $parsed_toppings['selections'] : array();
		$toppings_extra    = isset( $parsed_toppings['extra'] ) ? max( 0.0, (float) $parsed_toppings['extra'] ) : 0.0;
		$topping_tax_fees  = ! empty( $parsed_toppings['tax_fees'] ) && is_array( $parsed_toppings['tax_fees'] ) ? $parsed_toppings['tax_fees'] : array();
		$topping_tax_extra = $this->get_topping_tax_fee_unit_total( array( 'foodord_topping_tax_fees' => $topping_tax_fees ) );
		$signature         = isset( $parsed_toppings['signature'] ) ? sanitize_text_field( (string) $parsed_toppings['signature'] ) : '';
		$note              = $this->sanitize_cart_item_note( $note );
		$base_unit_price   = max( 0.0, (float) $product->get_price( 'edit' ) + $toppings_extra );
		$unit_price        = $base_unit_price + $topping_tax_extra;

		return array(
			'product_id'         => $product_id,
			'variation_id'       => $variation_id,
			'quantity'           => $quantity,
			'product_name'       => sanitize_text_field( $parent->get_name() ),
			'variation_name'     => $product instanceof WC_Product_Variation ? wp_strip_all_tags( wc_get_formatted_variation( $product, true, false, true ) ) : '',
			'unit_price'         => $unit_price,
			'base_unit_price'    => $base_unit_price,
			'toppings'           => $toppings,
			'toppings_extra'     => $toppings_extra,
			'topping_tax_fees'   => $topping_tax_fees,
			'toppings_signature' => $signature,
			'note'               => $note,
		);
	}

	/**
	 * Apply topping pricing immediately after WooCommerce builds the cart item.
	 *
	 * @param array  $cart_item Cart item data.
	 * @param string $cart_item_key Cart item key.
	 * @return array
	 */
	public function apply_toppings_to_cart_item( $cart_item, $cart_item_key ) {
		unset( $cart_item_key );
		return $this->apply_toppings_price( $cart_item );
	}

	/**
	 * Ensure topping surcharges are applied before totals.
	 *
	 * @param \WC_Cart $cart WooCommerce cart instance.
	 */
	public function enforce_toppings_pricing( $cart ) {
		$cart_class = 'WC_Cart';
		if ( ! $cart || ! is_a( $cart, $cart_class ) ) {
			return;
		}

		if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
			return;
		}

		foreach ( $cart->get_cart() as $key => $item ) {
			$cart->cart_contents[ $key ] = $this->apply_toppings_price( $item );
		}
	}

	/**
	 * Add tax-aware topping amounts to the WooCommerce cart as taxable fees.
	 *
	 * @param WC_Cart $cart WooCommerce cart.
	 */
	public function apply_topping_tax_fees( $cart ) {
		if ( ! $cart instanceof WC_Cart || ! wc_tax_enabled() ) {
			return;
		}

		$totals = array();
		foreach ( $cart->get_cart() as $cart_item ) {
			$quantity = max( 1, absint( $cart_item['quantity'] ?? 1 ) );
			foreach ( $this->get_topping_tax_fees( $cart_item ) as $fee ) {
				$tax_class = $fee['tax_class'];
				if ( ! isset( $totals[ $tax_class ] ) ) {
					$totals[ $tax_class ] = 0.0;
				}
				$totals[ $tax_class ] += $fee['price'] * $quantity;
			}
		}

		foreach ( $totals as $tax_class => $gross_amount ) {
			if ( $gross_amount <= 0 ) {
				continue;
			}

			$tax_label = $this->get_topping_tax_class_label( $tax_class );
			// translators: %s is a WooCommerce tax class name.
			$fee_name = sprintf( __( 'Toppings - %s', 'foodord' ), $tax_label );
			$cart->fees_api()->add_fee(
				array(
					'id'                           => 'foodord-toppings-tax-' . sanitize_key( $tax_class ),
					'name'                         => $fee_name,
					'amount'                       => $this->get_topping_fee_net_amount( $gross_amount, $tax_class, $cart->get_customer() ),
					'taxable'                      => true,
					'tax_class'                    => $this->resolve_woocommerce_tax_class( $tax_class ),
					'foodord_topping_tax_fee'      => true,
					'foodord_topping_tax_class'    => $tax_class,
					'foodord_topping_gross_amount' => $gross_amount,
				)
			);
		}
	}

	/**
	 * Hide internal topping tax fees while WooCommerce renders cart totals.
	 */
	public function hide_topping_tax_fees_from_totals() {
		if ( null !== $this->render_hidden_topping_tax_fees || ! function_exists( 'WC' ) || ! WC()->cart ) {
			return;
		}

		$fees         = WC()->cart->get_fees();
		$visible_fees = array();

		foreach ( $fees as $fee_id => $fee ) {
			if ( ! empty( $fee->foodord_topping_tax_fee ) ) {
				continue;
			}

			$visible_fees[ $fee_id ] = $fee;
		}

		if ( count( $visible_fees ) === count( $fees ) ) {
			return;
		}

		$this->render_hidden_topping_tax_fees = $fees;
		WC()->cart->fees_api()->set_fees( $visible_fees );
	}

	/**
	 * Restore topping tax fees after WooCommerce has rendered fee rows.
	 */
	public function restore_topping_tax_fees_after_totals() {
		if ( null === $this->render_hidden_topping_tax_fees || ! function_exists( 'WC' ) || ! WC()->cart ) {
			return;
		}

		WC()->cart->fees_api()->set_fees( $this->render_hidden_topping_tax_fees );
		$this->render_hidden_topping_tax_fees = null;
	}

	/**
	 * Hide internal topping tax fee rows from customer-facing order totals.
	 *
	 * The fee items remain on the order so its total and tax calculations stay
	 * unchanged. Only their presentation rows are removed.
	 *
	 * @param array    $total_rows Order total rows.
	 * @param WC_Order $order      WooCommerce order.
	 * @return array
	 */
	public function hide_topping_tax_fees_from_order_totals( $total_rows, $order ): array {
		if ( ! is_array( $total_rows ) || ! $order instanceof WC_Order ) {
			return is_array( $total_rows ) ? $total_rows : array();
		}

		foreach ( $order->get_fees() as $fee_item ) {
			if ( '1' !== (string) $fee_item->get_meta( '_foodord_topping_tax_fee', true ) ) {
				continue;
			}

			unset( $total_rows[ 'fee_' . $fee_item->get_id() ] );
		}

		return $total_rows;
	}

	/**
	 * Display inclusive tax classes on separate lines below the order total.
	 *
	 * @param string $html WooCommerce order total HTML.
	 * @return string
	 */
	public function format_order_total_tax_breakdown( $html ): string {
		$html = (string) $html;
		if ( false === strpos( $html, 'includes_tax' ) ) {
			return $html;
		}

		$html = preg_replace(
			'/<\/strong>\s*(?=<small\b[^>]*\bincludes_tax\b)/',
			'</strong><br>',
			$html,
			1
		);
		$html = preg_replace(
			'/,\s+(?=<span\b[^>]*\bwoocommerce-Price-amount\b)/',
			',<br>',
			$html
		);

		return is_string( $html ) ? $html : '';
	}

	/**
	 * Hide internal topping tax fee rows from WooCommerce Store API output.
	 *
	 * The cart remains unchanged so totals, taxes, and order creation continue
	 * to include the internal fees.
	 *
	 * @param WP_REST_Response|WP_HTTP_Response|mixed $response REST response.
	 * @param array|mixed                             $handler  Route handler.
	 * @param WP_REST_Request                         $request  REST request.
	 * @return mixed
	 */
	public function hide_topping_tax_fees_from_store_api( $response, $handler, $request ) {
		unset( $handler );

		if ( ! ( $response instanceof WP_HTTP_Response ) || ! ( $request instanceof WP_REST_Request ) ) {
			return $response;
		}

		$route = (string) $request->get_route();
		if ( 0 !== strpos( $route, '/wc/store/' ) ) {
			return $response;
		}

		$data = $response->get_data();
		if ( ! is_array( $data ) ) {
			return $response;
		}

		$data = $this->filter_store_api_topping_tax_fees( $data );
		if ( isset( $data['data']['cart'] ) && is_array( $data['data']['cart'] ) ) {
			$data['data']['cart'] = $this->filter_store_api_topping_tax_fees( $data['data']['cart'] );
		}

		$response->set_data( $data );

		return $response;
	}

	/**
	 * Remove FoodOrd internal topping tax fees from Store API presentation data.
	 *
	 * @param array $data Store API response data.
	 * @return array
	 */
	private function filter_store_api_topping_tax_fees( array $data ): array {
		if ( empty( $data['fees'] ) || ! is_array( $data['fees'] ) ) {
			return $data;
		}

		$data['fees'] = array_values(
			array_filter(
				$data['fees'],
				static function ( $fee ): bool {
					if ( ! is_array( $fee ) ) {
						return true;
					}

					$fee_key = isset( $fee['key'] ) ? (string) $fee['key'] : ( isset( $fee['id'] ) ? (string) $fee['id'] : '' );

					return 0 !== strpos( $fee_key, 'foodord-toppings-tax-' );
				}
			)
		);

		return $data;
	}

	/**
	 * Sanitize a stored topping tax class.
	 *
	 * @param mixed $tax_class Tax class value.
	 */
	private function sanitize_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 validated tax-aware topping fees from a cart-style item.
	 *
	 * @param array $cart_item Cart-style item data.
	 * @return array<int,array{id:string,label:string,price:float,tax_class:string}>
	 */
	private function get_topping_tax_fees( array $cart_item ): array {
		$raw_fees = isset( $cart_item['foodord_topping_tax_fees'] ) && is_array( $cart_item['foodord_topping_tax_fees'] ) ? $cart_item['foodord_topping_tax_fees'] : array();
		$fees     = array();

		foreach ( $raw_fees as $raw_fee ) {
			if ( ! is_array( $raw_fee ) ) {
				continue;
			}

			$tax_class = $this->sanitize_topping_tax_class( $raw_fee['tax_class'] ?? '' );
			$price     = max( 0.0, (float) ( $raw_fee['price'] ?? 0 ) );
			if ( '' === $tax_class || $price <= 0 ) {
				continue;
			}

			$fees[] = array(
				'id'        => sanitize_text_field( (string) ( $raw_fee['id'] ?? '' ) ),
				'label'     => sanitize_text_field( (string) ( $raw_fee['label'] ?? '' ) ),
				'price'     => $price,
				'tax_class' => $tax_class,
			);
		}

		return $fees;
	}

	/**
	 * Get the configured tax-aware topping amount for one product unit.
	 *
	 * @param array $cart_item Cart-style item data.
	 */
	private function get_topping_tax_fee_unit_total( array $cart_item ): float {
		$total = 0.0;
		foreach ( $this->get_topping_tax_fees( $cart_item ) as $fee ) {
			$total += $fee['price'];
		}

		return max( 0.0, $total );
	}

	/**
	 * Get all tax-aware topping amounts in a cart using configured display prices.
	 *
	 * @param WC_Cart $cart WooCommerce cart.
	 */
	private function get_cart_topping_tax_fee_total( WC_Cart $cart ): float {
		$total = 0.0;
		foreach ( $cart->get_cart() as $cart_item ) {
			$total += $this->get_topping_tax_fee_unit_total( $cart_item ) * max( 1, absint( $cart_item['quantity'] ?? 1 ) );
		}

		return max( 0.0, $total );
	}

	/**
	 * Map the internal Standard sentinel to WooCommerce's empty tax class slug.
	 *
	 * @param string $tax_class Internal tax class.
	 */
	private function resolve_woocommerce_tax_class( string $tax_class ): string {
		return 'standard' === $tax_class ? '' : $tax_class;
	}

	/**
	 * Get a readable tax class label.
	 *
	 * @param string $tax_class Internal tax class.
	 */
	private function get_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 = WC_Tax::get_tax_class_slugs();
			$key   = array_search( $tax_class, array_map( 'sanitize_title', $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 configured topping amount to a fee net amount.
	 *
	 * @param float       $gross_amount Gross amount from FoodOrd settings.
	 * @param string      $tax_class    Internal tax class.
	 * @param WC_Customer $customer     WooCommerce customer.
	 */
	private function get_topping_fee_net_amount( float $gross_amount, string $tax_class, $customer = null ): float {
		$gross_amount = max( 0.0, $gross_amount );
		if ( ! wc_tax_enabled() || ! wc_prices_include_tax() || ! class_exists( 'WC_Tax' ) ) {
			return $gross_amount;
		}

		$rates = WC_Tax::get_rates( $this->resolve_woocommerce_tax_class( $tax_class ), $customer );
		$taxes = WC_Tax::calc_tax( $gross_amount, $rates, true );
		return max( 0.0, $gross_amount - array_sum( $taxes ) );
	}

	/**
	 * Expose topping selections within cart/checkout item data.
	 *
	 * @param array $item_data Existing display data.
	 * @param array $cart_item Cart item data.
	 * @return array
	 */
	public function expose_topping_item_data( $item_data, $cart_item ) {
		if ( ! empty( $cart_item['foodord_toppings'] ) && is_array( $cart_item['foodord_toppings'] ) ) {
			// Build a lookup for hide_price and labels per set for this product.
			$hide_map            = array();
			$label_map           = array();
			$product_id_for_sets = isset( $cart_item['product_id'] ) ? (int) $cart_item['product_id'] : ( isset( $cart_item['data'] ) && $cart_item['data'] instanceof WC_Product ? (int) $cart_item['data']->get_id() : 0 );
			if ( $product_id_for_sets > 0 ) {
				$sets_for_product = $this->prepare_product_topping_sets( $product_id_for_sets );
				if ( is_array( $sets_for_product ) ) {
					foreach ( $sets_for_product as $s ) {
						if ( isset( $s['id'] ) ) {
							$set_id_key               = (string) $s['id'];
							$hide_map[ $set_id_key ]  = ! empty( $s['hide_price'] );
							$label_map[ $set_id_key ] = isset( $s['label'] ) ? sanitize_text_field( (string) $s['label'] ) : '';
						}
					}
				}
			}

			foreach ( $cart_item['foodord_toppings'] as $set ) {
				if ( empty( $set['items'] ) || ! is_array( $set['items'] ) ) {
					continue;
				}

				$set_name            = isset( $set['set_name'] ) && '' !== $set['set_name'] ? sanitize_text_field( (string) $set['set_name'] ) : __( 'Toppings', 'foodord' );
				$set_id              = isset( $set['set_id'] ) ? (string) $set['set_id'] : '';
				$hide_prices_for_set = ( '' !== $set_id && isset( $hide_map[ $set_id ] ) ) ? (bool) $hide_map[ $set_id ] : false;
				$raw_label           = isset( $set['set_label'] ) && '' !== $set['set_label'] ? sanitize_text_field( (string) $set['set_label'] ) : '';
				if ( '' === $raw_label && '' !== $set_id && isset( $label_map[ $set_id ] ) && '' !== $label_map[ $set_id ] ) {
					$raw_label = $label_map[ $set_id ];
				}
				$display_label    = '' !== $raw_label ? $raw_label : $set_name;
				$wc_display_label = $this->trim_trailing_label_colon( $display_label );
				$values           = array();
				foreach ( $set['items'] as $selection ) {
					$label = isset( $selection['label'] ) ? $selection['label'] : '';
					if ( '' === $label ) {
						continue;
					}
					$price = isset( $selection['price'] ) ? (float) $selection['price'] : 0.0;
					if ( ! $hide_prices_for_set && $price > 0 && function_exists( 'wc_price' ) ) {
						$raw_price  = html_entity_decode( wp_strip_all_tags( wc_price( $price ) ), ENT_QUOTES, 'UTF-8' );
						$normalized = trim( preg_replace( '/\x{00A0}/u', ' ', $raw_price ) );
						$values[]   = sprintf( '%s (+%s)', $label, $normalized );
						continue;
					}
					$values[] = $label;
				}

				if ( ! empty( $values ) ) {
					$values_plain  = array_map( 'sanitize_text_field', $values );
					$values_html   = array_map( 'esc_html', $values_plain );
					$display_block = implode( '<br />', $values_html );
					if ( '' !== $display_block ) {
						$set_attr      = '' !== $set_id ? ' data-foodord-topping-set="' . esc_attr( sanitize_html_class( $set_id ) ) . '"' : '';
						$display_block = '<span class="foodord-cart-topping-values" data-foodord-topping-values="1"' . $set_attr . '>' . $display_block . '</span>';
					}
					$item_data[] = array(
						'name'      => $wc_display_label,
						'value'     => implode( ', ', $values_plain ),
						'display'   => $display_block,
						'className' => 'foodord-block-checkout-topping',
					);
				}
			}
		}

		if ( isset( $cart_item['foodord_note'] ) && '' !== $cart_item['foodord_note'] ) {
			$item_data[] = array(
				'name'  => __( 'Note', 'foodord' ),
				'value' => $this->sanitize_cart_item_note( (string) $cart_item['foodord_note'] ),
			);
		}

		return $item_data;
	}

	/**
	 * Trim trailing colon characters from labels (WooCommerce adds them in some contexts).
	 *
	 * @param string $label Label text.
	 * @return string
	 */
	private function trim_trailing_label_colon( string $label ): string {
		$trimmed = trim( $label );
		if ( '' === $trimmed ) {
			return '';
		}
		$normalized = preg_replace( '/\s*[:：]\s*$/u', '', $trimmed );
		if ( is_string( $normalized ) && '' !== $normalized ) {
			return $normalized;
		}
		return $trimmed;
	}

	/**
	 * Apply price adjustments for toppings.
	 *
	 * @param array $cart_item Cart item data.
	 * @return array
	 */
	protected function apply_toppings_price( $cart_item ) {
		if ( empty( $cart_item['data'] ) || ! $cart_item['data'] instanceof WC_Product ) {
			return $cart_item;
		}

		if ( empty( $cart_item['foodord_toppings'] ) || ! is_array( $cart_item['foodord_toppings'] ) ) {
			return $cart_item;
		}

		$product = $cart_item['data'];
		$extra   = isset( $cart_item['foodord_toppings_extra'] ) ? max( 0, (float) $cart_item['foodord_toppings_extra'] ) : 0.0;
		$stored  = isset( $cart_item['foodord_toppings_base_price'] ) ? (float) $cart_item['foodord_toppings_base_price'] : null;
		$current = (float) $product->get_price( 'edit' );

		$base = $current;
		if ( null !== $stored ) {
			$epsilon = 0.01;
			if ( $extra > 0 && abs( $current - ( $stored + $extra ) ) < $epsilon ) {
				$base = $stored;
			} elseif ( $current > 0 && $current < $stored ) {
				$base = $current;
			} else {
				$base = $stored;
			}
		}

		$base                                     = max( 0, (float) $base );
		$cart_item['foodord_toppings_base_price'] = $base;
		$product->set_price( max( 0, $base + $extra ) );

		return $cart_item;
	}

	/**
	 * Display cart item price with toppings in cart.
	 *
	 * @param string $price_html Original price HTML.
	 * @param array  $cart_item Cart item data.
	 * @param string $cart_item_key Cart item key.
	 * @return string
	 */
	public function display_cart_item_price_with_toppings( $price_html, $cart_item, $cart_item_key ) {
		unset( $cart_item_key );

		if ( empty( $cart_item['foodord_toppings'] ) || ! is_array( $cart_item['foodord_toppings'] ) ) {
			return $price_html;
		}

		$product = $cart_item['data'];
		if ( ! $product instanceof WC_Product ) {
			return $price_html;
		}

		// Display the adjusted price (base + toppings).
		return wc_price( (float) $product->get_price() + $this->get_topping_tax_fee_unit_total( $cart_item ) );
	}

	/**
	 * Display cart item subtotal with toppings in cart.
	 *
	 * @param string $subtotal_html Original subtotal HTML.
	 * @param array  $cart_item Cart item data.
	 * @param string $cart_item_key Cart item key.
	 * @return string
	 */
	public function display_cart_item_subtotal_with_toppings( $subtotal_html, $cart_item, $cart_item_key ) {
		unset( $cart_item_key );

		if ( empty( $cart_item['foodord_toppings'] ) || ! is_array( $cart_item['foodord_toppings'] ) ) {
			return $subtotal_html;
		}

		$product = $cart_item['data'];
		if ( ! $product instanceof WC_Product ) {
			return $subtotal_html;
		}

		// Display the adjusted subtotal.
		$quantity   = isset( $cart_item['quantity'] ) ? (int) $cart_item['quantity'] : 1;
		$unit_price = (float) $product->get_price() + $this->get_topping_tax_fee_unit_total( $cart_item );
		return wc_price( $unit_price * $quantity );
	}

	/**
	 * Add toppings data to order line item on checkout.
	 *
	 * @param WC_Order_Item_Product $item Order line item.
	 * @param string                $cart_item_key Cart item key.
	 * @param array                 $cart_item Cart item data.
	 * @param WC_Order              $order Order object.
	 */
	public function add_toppings_to_order_line_item( $item, $cart_item_key, $cart_item, $order ) {
		unset( $cart_item_key, $order );

		if ( ! empty( $cart_item['foodord_toppings'] ) && is_array( $cart_item['foodord_toppings'] ) ) {
			// Store toppings data as order item meta when available.
			$item->add_meta_data( '_foodord_toppings', $cart_item['foodord_toppings'], true );

			$show_group_label = 'yes' === get_option( 'foodord_show_topping_group_label', 'yes' );
			$group_strings    = array();

			foreach ( $cart_item['foodord_toppings'] as $set ) {
				if ( empty( $set['items'] ) || ! is_array( $set['items'] ) ) {
					continue;
				}

				$set_label = '';
				if ( ! empty( $set['set_label'] ) ) {
					$set_label = trim( wp_strip_all_tags( (string) $set['set_label'] ) );
				} elseif ( ! empty( $set['set_name'] ) ) {
					$set_label = trim( wp_strip_all_tags( (string) $set['set_name'] ) );
				}

				$lines = array();
				foreach ( $set['items'] as $selection ) {
					$label = isset( $selection['label'] ) ? trim( wp_strip_all_tags( (string) $selection['label'] ) ) : '';
					if ( '' === $label ) {
						continue;
					}
					$price = isset( $selection['price'] ) ? (float) $selection['price'] : 0.0;
					if ( $price > 0 ) {
						$price_text = function_exists( 'wc_price' ) ? wp_strip_all_tags( wc_price( $price ) ) : number_format( $price, 2 );
						$label     .= ' (+' . $price_text . ')';
					}
					$lines[] = $label;
				}

				if ( empty( $lines ) ) {
					continue;
				}

				$group_text = implode( PHP_EOL, $lines );
				if ( $show_group_label && '' !== $set_label ) {
					$group_text = $set_label . PHP_EOL . $group_text;
				}

				$group_strings[] = $group_text;
			}

			if ( ! empty( $group_strings ) ) {
				$item->add_meta_data( __( 'Toppings', 'foodord' ), implode( PHP_EOL . PHP_EOL, $group_strings ), true );
			}

			if ( isset( $cart_item['foodord_toppings_extra'] ) ) {
				$item->add_meta_data( '_foodord_toppings_extra', $cart_item['foodord_toppings_extra'], true );
			}

			if ( ! empty( $cart_item['foodord_topping_tax_fees'] ) && is_array( $cart_item['foodord_topping_tax_fees'] ) ) {
				$item->add_meta_data( '_foodord_topping_tax_fees', $cart_item['foodord_topping_tax_fees'], true );
			}

			if ( isset( $cart_item['foodord_toppings_signature'] ) ) {
				$item->add_meta_data( '_foodord_toppings_signature', $cart_item['foodord_toppings_signature'], true );
			}

			if ( isset( $cart_item['foodord_toppings_base_price'] ) ) {
				$item->add_meta_data( '_foodord_toppings_base_price', $cart_item['foodord_toppings_base_price'], true );
			}
		}

		if ( isset( $cart_item['foodord_note'] ) && '' !== $cart_item['foodord_note'] ) {
			$item->add_meta_data( '_foodord_note', $this->sanitize_cart_item_note( (string) $cart_item['foodord_note'] ), true );
		}
	}

	/**
	 * Preserve FoodOrd markers on WooCommerce topping fee order lines.
	 *
	 * @param WC_Order_Item_Fee $item    Order fee item.
	 * @param string            $fee_key Cart fee key.
	 * @param object            $fee     Cart fee object.
	 * @param WC_Order          $order   Order object.
	 */
	public function mark_topping_tax_order_fee( $item, $fee_key, $fee, $order ) {
		unset( $fee_key, $order );

		if ( ! $item instanceof WC_Order_Item_Fee || empty( $fee->foodord_topping_tax_fee ) ) {
			return;
		}

		$item->add_meta_data( '_foodord_topping_tax_fee', '1', true );
		if ( isset( $fee->foodord_topping_tax_class ) ) {
			$item->add_meta_data( '_foodord_topping_tax_class', sanitize_title( (string) $fee->foodord_topping_tax_class ), true );
		}
	}

	/**
	 * Determine if the active theme already manages FoodOrd order item meta output.
	 *
	 * @return bool
	 */
	private function is_foodord_theme_handling_order_item_meta(): bool {
		return function_exists( 'foodord_filter_internal_order_item_meta' ) || function_exists( 'foodord_render_public_order_item_toppings' );
	}

	/**
	 * Hide internal FoodOrd meta rows from formatted order item meta output.
	 * Prevents duplicate "Toppings" output when structured toppings are rendered separately.
	 *
	 * @param array $formatted_meta Formatted meta objects keyed by meta ID.
	 * @param mixed $item           Order item instance.
	 * @return array
	 */
	public function filter_internal_order_item_meta( $formatted_meta, $item ) {
		unset( $item );

		if ( empty( $formatted_meta ) || ! is_array( $formatted_meta ) ) {
			return $formatted_meta;
		}

		$toppings_label_key = sanitize_key( __( 'Toppings', 'foodord' ) );

		foreach ( $formatted_meta as $meta_id => $meta ) {
			if ( ! is_object( $meta ) ) {
				continue;
			}

			$raw_key      = isset( $meta->key ) ? (string) $meta->key : '';
			$raw_key_norm = sanitize_key( $raw_key );
			if ( '' !== $raw_key_norm && 0 === strpos( $raw_key_norm, '_foodord_toppings' ) ) {
				unset( $formatted_meta[ $meta_id ] );
				continue;
			}

			$display_key      = isset( $meta->display_key ) ? (string) $meta->display_key : '';
			$display_key_norm = sanitize_key( wp_strip_all_tags( $display_key ) );

			if ( 'toppings' === $raw_key_norm || 'toppings' === $display_key_norm ) {
				unset( $formatted_meta[ $meta_id ] );
				continue;
			}

			if ( '' !== $toppings_label_key && ( $toppings_label_key === $raw_key_norm || $toppings_label_key === $display_key_norm ) ) {
				unset( $formatted_meta[ $meta_id ] );
				continue;
			}
		}

		return $formatted_meta;
	}

	/**
	 * Build plain-text FoodOrd metadata for an order item.
	 *
	 * @param WC_Order_Item $item Order item instance.
	 * @return string
	 */
	public function get_order_item_additional_meta_text( $item ): string {
		if ( ! $item instanceof WC_Order_Item ) {
			return '';
		}

		$sections = array();
		foreach ( $this->get_order_item_toppings_sets( $item ) as $set ) {
			$lines = array();
			if ( ! empty( $set['show_label'] ) && ! empty( $set['label'] ) ) {
				$lines[] = (string) $set['label'] . ':';
			}

			foreach ( $set['items'] as $line ) {
				$lines[] = '- ' . $line;
			}

			if ( ! empty( $lines ) ) {
				$sections[] = implode( PHP_EOL, $lines );
			}
		}

		$note = $this->sanitize_cart_item_note( (string) $item->get_meta( '_foodord_note', true ) );
		if ( '' !== $note ) {
			$sections[] = esc_html__( 'Note:', 'foodord' ) . ' ' . $note;
		}

		return implode( PHP_EOL . PHP_EOL, $sections );
	}

	/**
	 * Render toppings on order details pages (thank you, account, emails).
	 *
	 * @param int              $item_id    Order item ID.
	 * @param WC_Order_Item    $item       Order item object.
	 * @param WC_Order|bool    $order      Order context.
	 * @param bool|string|null $plain_text Whether the context expects plain text output.
	 */
	public function render_public_order_item_toppings( $item_id, $item, $order, $plain_text = false ): void {
		unset( $item_id, $order );

		if ( is_admin() && ! $plain_text ) {
			return;
		}

		if ( ! $item || ! is_object( $item ) || ! method_exists( $item, 'get_meta' ) ) {
			return;
		}

		$sets = $this->get_order_item_toppings_sets( $item );
		if ( empty( $sets ) ) {
			return;
		}

		if ( $plain_text ) {
			$chunks = array();
			foreach ( $sets as $set ) {
				$lines = array();
				if ( ! empty( $set['show_label'] ) && ! empty( $set['label'] ) ) {
					$lines[] = (string) $set['label'];
				}
				foreach ( $set['items'] as $line ) {
					$lines[] = '- ' . $line;
				}
				if ( ! empty( $lines ) ) {
					$chunks[] = implode( PHP_EOL, $lines );
				}
			}

			$plain = ! empty( $chunks ) ? implode( PHP_EOL . PHP_EOL, $chunks ) : '';
			if ( '' !== $plain ) {
				echo PHP_EOL . esc_html( $plain ) . PHP_EOL;
			}
			return;
		}

		$markup = '<ul class="wc-item-meta">';
		foreach ( $sets as $set ) {
			$items_html = implode( '<br />', array_map( 'esc_html', $set['items'] ) );
			if ( ! empty( $set['show_label'] ) && ! empty( $set['label'] ) ) {
				$markup .= '<li><strong class="wc-item-meta-label">' . esc_html( $set['label'] ) . ':</strong> ' . $items_html . '</li>';
				continue;
			}
			$markup .= '<li>' . $items_html . '</li>';
		}
		$markup .= '</ul>';

		echo wp_kses_post( $markup );
	}

	/**
	 * Render a product note on order details pages and in emails.
	 *
	 * @param int              $item_id    Order item ID.
	 * @param WC_Order_Item    $item       Order item object.
	 * @param WC_Order|bool    $order      Order context.
	 * @param bool|string|null $plain_text Whether plain text output is required.
	 */
	public function render_public_order_item_note( $item_id, $item, $order, $plain_text = false ): void {
		unset( $item_id, $order );

		if ( is_admin() && ! $plain_text ) {
			return;
		}

		$note = $item instanceof WC_Order_Item ? $this->sanitize_cart_item_note( (string) $item->get_meta( '_foodord_note', true ) ) : '';
		if ( '' === $note ) {
			return;
		}

		if ( $plain_text ) {
			echo PHP_EOL . esc_html__( 'Note:', 'foodord' ) . ' ' . esc_html( $note ) . PHP_EOL;
			return;
		}

		echo wp_kses_post( '<div class="foodord-order-item-note"><strong>' . esc_html__( 'Note:', 'foodord' ) . '</strong> ' . esc_html( $note ) . '</div>' );
	}

	/**
	 * Render a product note below an item in the WooCommerce order editor.
	 *
	 * @param int           $item_id Order item ID.
	 * @param WC_Order_Item $item    Order item object.
	 * @param WC_Product    $product Product object.
	 */
	public function render_admin_order_item_note( $item_id, $item, $product ): void {
		unset( $item_id, $product );

		if ( ! is_admin() || ! $item instanceof WC_Order_Item ) {
			return;
		}

		$note = $this->sanitize_cart_item_note( (string) $item->get_meta( '_foodord_note', true ) );
		if ( '' !== $note ) {
			echo wp_kses_post( '<div class="foodord-admin-order-item-note"><strong>' . esc_html__( 'Note:', 'foodord' ) . '</strong> ' . esc_html( $note ) . '</div>' );
		}
	}

	/**
	 * Stack variation and topping metadata in reservation pre-order emails.
	 *
	 * @param string $css   WooCommerce email styles.
	 * @param mixed  $email WooCommerce email instance.
	 * @return string
	 */
	public function append_preorder_item_meta_email_styles( $css, $email ): string {
		$order = is_object( $email ) && isset( $email->object ) && $email->object instanceof WC_Order
			? $email->object
			: false;
		if ( ! $order || ! class_exists( 'Foodord_Table_Reservation_Deposits' ) || ! Foodord_Table_Reservation_Deposits::is_preorder_order( $order ) ) {
			return (string) $css;
		}

		return (string) $css . '
.wc-item-meta,
.foodord-order-item-toppings,
.foodord-order-item-toppings__list,
.foodord-order-item-note {
	display: block;
}

.wc-item-meta li,
.foodord-order-item-toppings__set,
.foodord-order-item-topping {
	display: block;
	width: 100%;
}

.wc-item-meta li + li,
.foodord-order-item-toppings__set + .foodord-order-item-toppings__set,
.foodord-order-item-topping + .foodord-order-item-topping {
	margin-top: 4px;
}

.wc-item-meta .wc-item-meta-label,
.wc-item-meta li p {
	display: block;
	float: none;
	margin: 0;
}

.foodord-order-item-note {
	margin-top: 6px;
}
';
	}

	/**
	 * Prepare toppings data for order item displays.
	 *
	 * @param WC_Order_Item $item Order item.
	 * @return array<int, array{label:string, show_label:bool, items:array<int,string>}>
	 */
	private function get_order_item_toppings_sets( $item ): array {
		$raw_toppings = $item->get_meta( '_foodord_toppings', true );
		if ( empty( $raw_toppings ) || ! is_array( $raw_toppings ) ) {
			return array();
		}

		$label_map = array();
		$hide_map  = array();
		$sets_opt  = get_option( 'foodord_topping_sets', array() );
		if ( is_array( $sets_opt ) ) {
			foreach ( $sets_opt as $set ) {
				if ( ! is_array( $set ) || empty( $set['id'] ) ) {
					continue;
				}
				$set_id               = sanitize_text_field( (string) $set['id'] );
				$label_map[ $set_id ] = isset( $set['label'] ) ? sanitize_text_field( (string) $set['label'] ) : '';
				$hide_map[ $set_id ]  = ! empty( $set['hide_price'] );
			}
		}

		$show_labels = 'yes' === get_option( 'foodord_show_topping_group_label', 'yes' );
		$result      = array();

		foreach ( $raw_toppings as $set ) {
			if ( ! is_array( $set ) || empty( $set['items'] ) || ! is_array( $set['items'] ) ) {
				continue;
			}

			$set_id       = isset( $set['set_id'] ) ? (string) $set['set_id'] : '';
			$set_name_raw = isset( $set['set_name'] ) ? (string) $set['set_name'] : '';
			$set_label    = isset( $set['set_label'] ) ? (string) $set['set_label'] : '';
			$hide_prices  = ( '' !== $set_id && isset( $hide_map[ $set_id ] ) ) ? (bool) $hide_map[ $set_id ] : false;

			$display = '';
			if ( '' !== $set_label ) {
				$display = $set_label;
			} elseif ( '' !== $set_name_raw ) {
				$display = $set_name_raw;
			} elseif ( '' !== $set_id && isset( $label_map[ $set_id ] ) && '' !== $label_map[ $set_id ] ) {
				$display = $label_map[ $set_id ];
			}
			$display = trim( wp_strip_all_tags( $display ) );

			$items_plain = array();
			foreach ( $set['items'] as $selection ) {
				$label = '';
				$price = 0.0;
				if ( is_array( $selection ) ) {
					$label = isset( $selection['label'] ) ? (string) $selection['label'] : '';
					$price = isset( $selection['price'] ) ? (float) $selection['price'] : 0.0;
				} elseif ( is_string( $selection ) || is_numeric( $selection ) ) {
					$label = (string) $selection;
				}
				$label = trim( wp_strip_all_tags( $label ) );
				if ( '' === $label ) {
					continue;
				}

				$line = $label;
				if ( ! $hide_prices && $price > 0 ) {
					$price_text = function_exists( 'wc_price' ) ? wp_strip_all_tags( wc_price( $price ) ) : number_format( $price, 2 );
					$line      .= ' (+' . $price_text . ')';
				}
				$items_plain[] = $line;
			}

			if ( empty( $items_plain ) ) {
				continue;
			}

			$result[] = array(
				'label'      => $display,
				'show_label' => $show_labels && '' !== $display,
				'items'      => $items_plain,
			);
		}

		return $result;
	}

	/**
	 * Sanitize toppings payload coming from requests.
	 *
	 * @param mixed $raw Raw request payload.
	 * @return array
	 */
	private function sanitize_toppings_request_payload( $raw ): array {
		if ( ! is_array( $raw ) ) {
			return array();
		}

		$sanitized = array();
		foreach ( $raw as $set_id => $value ) {
			$clean_set_id = sanitize_text_field( (string) $set_id );
			if ( '' === $clean_set_id ) {
				continue;
			}

			if ( is_array( $value ) ) {
				$items = array();
				foreach ( $value as $single ) {
					if ( is_array( $single ) ) {
						continue;
					}
					$clean = sanitize_text_field( (string) $single );
					if ( '' !== $clean ) {
						$items[] = $clean;
					}
				}
				$sanitized[ $clean_set_id ] = $items;
				continue;
			}

			$sanitized[ $clean_set_id ] = sanitize_text_field( (string) $value );
		}

		return $sanitized;
	}

	/**
	 * Sanitize and normalize cart item note values.
	 *
	 * @param string $note Raw note value.
	 * @return string
	 */
	private function sanitize_cart_item_note( string $note ): string {
		$note = wp_strip_all_tags( $note );
		$note = preg_replace( '/\s+/u', ' ', $note );
		$note = trim( (string) $note );

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

		if ( function_exists( 'mb_substr' ) ) {
			return mb_substr( $note, 0, self::CART_NOTE_MAX_LENGTH );
		}

		return substr( $note, 0, self::CART_NOTE_MAX_LENGTH );
	}

	/**
	 * Determine the current order mode with session/cookie fallback.
	 *
	 * @return string
	 */
	private function resolve_order_mode(): string {
		if ( function_exists( 'foodord_get_order_mode' ) ) {
			return foodord_get_order_mode();
		}
		if ( function_exists( 'WC' ) && WC()->session ) {
			$session_mode = (string) WC()->session->get( 'foodord_order_mode', 'delivery' );
			if ( in_array( $session_mode, array( 'delivery', 'pickup' ), true ) ) {
				return $session_mode;
			}
		}
		return 'delivery';
	}

	/**
	 * Calculate delivery fee details for the active ordering mode.
	 *
	 * @param string $mode Current order mode (delivery|pickup).
	 * @return array{amount: float, html: string}
	 */
	private function get_delivery_fee_details( string $mode ): array {
		$amount = 0.0;
		$html   = '';

		if ( 'pickup' === $mode ) {
			return array(
				'amount' => 0.0,
				'html'   => '',
			);
		}

		$type = get_option( 'foodord_delivery_fee_type', 'fixed' );

		// If there is no allowed address in session, do not expose any delivery fee (for any type).
		$session     = class_exists( 'Foodord_Location_Service' ) ? Foodord_Location_Service::get_session() : array();
		$has_allowed = ! empty( $session['allowed'] );
		if ( ! $has_allowed ) {
			return array(
				'amount' => 0.0,
				'html'   => '',
			);
		}

		if ( 'fixed' === $type ) {
			$raw = get_option( 'foodord_delivery_fee_fixed', '0' );
			if ( function_exists( 'wc_format_decimal' ) ) {
				$amount = (float) wc_format_decimal( $raw );
			} else {
				$amount = (float) $raw;
			}
		} elseif ( 'distance' === $type && class_exists( 'Foodord_Location_Service' ) ) {
			$delivery_mode = Foodord_Location_Service::get_delivery_mode();
			$radius_zone   = 'radius' === $delivery_mode ? Foodord_Location_Service::resolve_current_radius_zone_for_cart( function_exists( 'WC' ) ? WC()->cart : null ) : null;
			$session_fee   = isset( $session['fee'] ) ? (float) $session['fee'] : 0.0;
			if ( is_array( $radius_zone ) ) {
				$amount = isset( $radius_zone['fee'] ) ? max( 0.0, (float) $radius_zone['fee'] ) : 0.0;
			} elseif ( $session_fee > 0 ) {
				$amount = max( 0.0, $session_fee );
			} elseif ( 'postal' === $delivery_mode ) {
				$city   = isset( $session['city'] ) ? (string) $session['city'] : '';
				$postal = isset( $session['postal'] ) ? (string) $session['postal'] : '';
				if ( '' !== $city ) {
					$amount = Foodord_Location_Service::fee_for_city_postal( $city, $postal );
				}
			}
		}

		if ( $amount > 0 ) {
			$html = function_exists( 'wc_price' ) ? wc_price( $amount ) : number_format( $amount, 2 );
		}

		return array(
			'amount' => $amount,
			'html'   => $html,
		);
	}

	/**
	 * Calculate minimum order details for the active ordering mode.
	 * Applies only for delivery mode and when a valid allowed address is set in session.
	 *
	 * @param string $mode Current order mode (delivery|pickup).
	 * @return array{amount: float, html: string, active: bool}
	 */
	private function get_minimum_order_details( string $mode ): array {
		$amount = 0.0;
		$html   = '';
		$active = false;

		if ( 'pickup' === $mode ) {
			return array(
				'amount' => 0.0,
				'html'   => '',
				'active' => false,
			);
		}

		$session     = class_exists( 'Foodord_Location_Service' ) ? Foodord_Location_Service::get_session() : array();
		$has_allowed = ! empty( $session['allowed'] );
		if ( ! $has_allowed ) {
			return array(
				'amount' => 0.0,
				'html'   => '',
				'active' => false,
			);
		}

		$session_minimum = isset( $session['minimum'] ) ? max( 0.0, (float) $session['minimum'] ) : 0.0;
		$configured_mode = class_exists( 'Foodord_Location_Service' ) ? Foodord_Location_Service::get_delivery_mode() : 'postal';

		if ( 'radius' === $configured_mode ) {
			$radius_zone = Foodord_Location_Service::resolve_current_radius_zone_for_cart( function_exists( 'WC' ) ? WC()->cart : null );
			$amount      = is_array( $radius_zone ) && isset( $radius_zone['minimum'] ) ? max( 0.0, (float) $radius_zone['minimum'] ) : $session_minimum;
		} else {
			$city   = isset( $session['city'] ) ? (string) $session['city'] : '';
			$postal = isset( $session['postal'] ) ? (string) $session['postal'] : '';
			if ( '' !== $city ) {
				$location = class_exists( 'Foodord_Location_Service' ) ? Foodord_Location_Service::find_location( $city, $postal ) : null;
				if ( is_array( $location ) ) {
					$raw_min = isset( $location['minimum'] ) ? (string) $location['minimum'] : '';
					if ( '' !== $raw_min ) {
						if ( function_exists( 'wc_format_decimal' ) ) {
							$amount = (float) wc_format_decimal( $raw_min );
						} else {
							$amount = (float) $raw_min;
						}
						$amount = max( 0.0, $amount );
					}
				}
			}

			if ( $amount <= 0 && $session_minimum > 0 ) {
				$amount = $session_minimum;
			}
		}

		if ( $amount > 0 ) {
			$active = true;
			$html   = function_exists( 'wc_price' ) ? wc_price( $amount ) : number_format( $amount, 2 );
		}

		return array(
			'amount' => $amount,
			'html'   => $html,
			'active' => $active,
		);
	}
}
