<?php
/**
 * Table ordering REST controller.
 *
 * @package FoodOrd
 */

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

/**
 * Class Foodord_Table_Ordering
 */
class Foodord_Table_Ordering {
	private const OPTION_CONFIG = 'foodord_table_ordering_config';
	private const OPTION_ORDERS = 'foodord_table_ordering_orders';
	private const TOKEN_PARAM = 'foodord_table_token';
	private const ORDER_RETENTION_DAYS = 7;
	private const MAX_STORED_ORDERS = 300;

	/**
	 * REST namespace.
	 *
	 * @var string
	 */
	private $namespace = 'foodord/v1';

	/**
	 * Initialize hooks.
	 */
	public function init(): void {
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
		add_action( 'template_redirect', array( $this, 'redirect_unavailable_token_url' ) );
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_frontend_assets' ) );
	}

	/**
	 * Redirect unusable table ordering URLs back to the normal page.
	 */
	public function redirect_unavailable_token_url(): void {
		if ( is_admin() || wp_doing_ajax() ) {
			return;
		}

		$token = foodord_get_table_ordering_token();
		if ( '' === $token ) {
			return;
		}

		$record = $this->resolve_token_record( $token );
		if ( ! is_wp_error( $record ) ) {
			return;
		}

		$redirect_url = remove_query_arg( self::TOKEN_PARAM );
		if ( ! is_string( $redirect_url ) || '' === $redirect_url ) {
			$redirect_url = home_url( '/' );
		}

		wp_safe_redirect( esc_url_raw( $redirect_url ), 302 );
		exit;
	}

	/**
	 * Enqueue frontend behavior only for tokenized table ordering URLs.
	 */
	public function enqueue_frontend_assets(): void {
		$token = foodord_get_table_ordering_token();
		if ( '' === $token ) {
			return;
		}

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

		wp_enqueue_script(
			'foodord-table-ordering',
			FOODORD_URL . 'public/js/foodord-table-ordering.js',
			array( 'foodord-card', 'foodord-mobile-cart' ),
			FOODORD_VERSION,
			true
		);

		wp_localize_script(
			'foodord-table-ordering',
			'foodordTableOrdering',
			array(
				'restUrl'   => esc_url_raw( rest_url( $this->namespace . '/table-ordering' ) ),
				'restNonce' => is_user_logged_in() ? wp_create_nonce( 'wp_rest' ) : '',
				'token'     => $token,
				'i18n'      => array(
					'catalogOnly'      => __( 'This QR code is catalog only.', 'foodord' ),
					'sendOrder'        => __( 'Place order', 'foodord' ),
					'orderSent'        => __( 'Order sent.', 'foodord' ),
					'orderFailed'      => __( 'Unable to send order.', 'foodord' ),
					'orderPlacedTitle' => __( 'Order placed successfully!', 'foodord' ),
					'kitchenMessage'   => __( 'Your order has been sent to the kitchen.', 'foodord' ),
					'prepareMessage'   => __( "We'll prepare your order with care.", 'foodord' ),
					'continueLabel'    => __( 'Continue', 'foodord' ),
					'closeLabel'       => __( 'Close', 'foodord' ),
				),
			)
		);
	}

	/**
	 * Register REST routes.
	 */
	public function register_routes(): void {
		register_rest_route(
			$this->namespace,
			'/table-ordering/config',
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'handle_get_config' ),
				'permission_callback' => array( $this, 'check_authentication' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/table-ordering/config',
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'handle_update_config' ),
				'permission_callback' => array( $this, 'check_authentication' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/table-ordering/resolve',
			array(
				'methods'             => array( 'GET', 'POST' ),
				'callback'            => array( $this, 'handle_resolve_token' ),
				'permission_callback' => '__return_true',
			)
		);

		register_rest_route(
			$this->namespace,
			'/table-ordering/orders',
			array(
				'methods'             => 'GET',
				'callback'            => array( $this, 'handle_get_orders' ),
				'permission_callback' => array( $this, 'check_authentication' ),
			)
		);

		register_rest_route(
			$this->namespace,
			'/table-ordering/orders',
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'handle_create_order' ),
				'permission_callback' => '__return_true',
			)
		);

		register_rest_route(
			$this->namespace,
			'/table-ordering/orders/(?P<id>[A-Za-z0-9_-]+)',
			array(
				'methods'             => 'POST',
				'callback'            => array( $this, 'handle_update_order_status' ),
				'permission_callback' => array( $this, 'check_authentication' ),
			)
		);
	}

	/**
	 * Reuse the existing desktop authentication policy.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return bool|WP_Error
	 */
	public function check_authentication( $request ) {
		if ( ! class_exists( 'Foodord_Rest_Api' ) ) {
			return new WP_Error(
				'foodord_rest_unavailable',
				__( 'FoodOrd REST API is unavailable.', 'foodord' ),
				array( 'status' => 500 )
			);
		}

		$rest_api = new Foodord_Rest_Api();
		return $rest_api->check_authentication( $request );
	}

	/**
	 * Handle config fetch.
	 *
	 * @return WP_REST_Response
	 */
	public function handle_get_config(): WP_REST_Response {
		return new WP_REST_Response(
			array(
				'ok'     => true,
				'config' => $this->get_public_admin_config(),
			),
			200
		);
	}

	/**
	 * Handle config update.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function handle_update_config( WP_REST_Request $request ) {
		$payload = $request->get_json_params();
		$payload = is_array( $payload ) ? $payload : array();
		$current = $this->get_config();
		$config  = $this->normalize_config_payload( $payload, $current );

		update_option( self::OPTION_CONFIG, $config, false );

		return new WP_REST_Response(
			array(
				'ok'     => true,
				'config' => $this->get_public_admin_config( $config ),
			),
			200
		);
	}

	/**
	 * Resolve a public table ordering token.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function handle_resolve_token( WP_REST_Request $request ) {
		$token  = $this->read_token_from_request( $request );
		$record = $this->resolve_token_record( $token );

		if ( is_wp_error( $record ) ) {
			return new WP_REST_Response(
				array(
					'ok'       => false,
					'code'     => $record->get_error_code(),
					'message'  => $record->get_error_message(),
					'mode'     => 'invalid',
					'canOrder' => false,
					'table'    => null,
				),
				200
			);
		}

		return new WP_REST_Response(
			array(
				'ok'       => true,
				'mode'     => $record['mode'],
				'canOrder' => 'online' === $record['mode'],
				'table'    => isset( $record['table'] ) ? $record['table'] : null,
			),
			200
		);
	}

	/**
	 * Handle queued order fetch for desktop.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response
	 */
	public function handle_get_orders( WP_REST_Request $request ): WP_REST_Response {
		$status = sanitize_key( (string) $request->get_param( 'status' ) );
		if ( '' === $status ) {
			$status = 'pending';
		}

		$store  = $this->get_orders_store();
		$orders = isset( $store['orders'] ) && is_array( $store['orders'] ) ? $store['orders'] : array();
		$orders = array_values(
			array_filter(
				$orders,
				static function ( $order ) use ( $status ) {
					return is_array( $order ) && (string) ( $order['status'] ?? '' ) === $status;
				}
			)
		);

		return new WP_REST_Response(
			array(
				'ok'     => true,
				'orders' => $this->strip_private_order_fields( $orders ),
			),
			200
		);
	}

	/**
	 * Queue a public table ordering request.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function handle_create_order( WP_REST_Request $request ) {
		$payload = $request->get_json_params();
		$payload = is_array( $payload ) ? $payload : array();
		$token   = isset( $payload['token'] ) ? (string) $payload['token'] : $this->read_token_from_request( $request );
		$record  = $this->resolve_token_record( $token );

		if ( is_wp_error( $record ) ) {
			return $record;
		}

		if ( 'online' !== $record['mode'] || empty( $record['table'] ) ) {
			return new WP_Error(
				'foodord_table_ordering_catalog_only',
				__( 'Ordering is not available for this QR code.', 'foodord' ),
				array( 'status' => 403 )
			);
		}

		$items = $this->normalize_cart_order_items();
		if ( empty( $items ) && ! empty( $payload['items'] ) ) {
			$items = $this->normalize_order_items( $payload['items'] );
		}
		if ( is_wp_error( $items ) ) {
			return $items;
		}

		if ( empty( $items ) ) {
			return new WP_Error(
				'foodord_table_ordering_empty_order',
				__( 'Order is empty.', 'foodord' ),
				array( 'status' => 400 )
			);
		}

		$token_hash      = $this->hash_token( $token );
		$client_order_id = sanitize_text_field( (string) ( $payload['clientOrderId'] ?? $payload['client_order_id'] ?? '' ) );
		$store           = $this->get_orders_store();
		$existing_order  = $this->find_existing_order( $store, $token_hash, $client_order_id );

		if ( $existing_order ) {
			return new WP_REST_Response(
				array(
					'ok'          => true,
					'orderId'     => $existing_order['id'],
					'idempotent'  => true,
					'status'      => $existing_order['status'],
					'queuedOrder' => $this->strip_private_order_fields( array( $existing_order ) )[0],
				),
				200
			);
		}

		$now   = gmdate( 'c' );
		$order = array(
			'id'            => $this->generate_order_id(),
			'status'        => 'pending',
			'tokenHash'     => $token_hash,
			'clientOrderId' => $client_order_id,
			'table'         => $record['table'],
			'items'         => $items,
			'total'         => $this->calculate_order_total( $items ),
			'currency'      => function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : '',
			'createdAt'     => $now,
			'updatedAt'     => $now,
		);

		$store['orders'][] = $order;
		$store             = $this->prune_orders_store( $store );
		update_option( self::OPTION_ORDERS, $store, false );
		$this->clear_customer_cart();

		return new WP_REST_Response(
			array(
				'ok'          => true,
				'orderId'     => $order['id'],
				'status'      => 'pending',
				'queuedOrder' => $this->strip_private_order_fields( array( $order ) )[0],
			),
			201
		);
	}

	/**
	 * Update a queued order status after desktop processing.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return WP_REST_Response|WP_Error
	 */
	public function handle_update_order_status( WP_REST_Request $request ) {
		$order_id = sanitize_text_field( (string) $request->get_param( 'id' ) );
		$payload  = $request->get_json_params();
		$payload  = is_array( $payload ) ? $payload : array();
		$status   = sanitize_key( (string) ( $payload['status'] ?? '' ) );

		if ( ! in_array( $status, array( 'pending', 'received', 'failed' ), true ) ) {
			return new WP_Error(
				'foodord_table_ordering_invalid_status',
				__( 'Invalid order status.', 'foodord' ),
				array( 'status' => 400 )
			);
		}

		$store = $this->get_orders_store();
		$found = false;
		$now   = gmdate( 'c' );

		foreach ( $store['orders'] as $index => $order ) {
			if ( ! is_array( $order ) || (string) ( $order['id'] ?? '' ) !== $order_id ) {
				continue;
			}

			$store['orders'][ $index ]['status']    = $status;
			$store['orders'][ $index ]['updatedAt'] = $now;
			if ( 'failed' === $status ) {
				$store['orders'][ $index ]['error'] = sanitize_text_field( (string) ( $payload['error'] ?? '' ) );
			} else {
				unset( $store['orders'][ $index ]['error'] );
			}

			$found = true;
			break;
		}

		if ( ! $found ) {
			return new WP_Error(
				'foodord_table_ordering_order_not_found',
				__( 'Queued order was not found.', 'foodord' ),
				array( 'status' => 404 )
			);
		}

		$store = $this->prune_orders_store( $store );
		update_option( self::OPTION_ORDERS, $store, false );

		return new WP_REST_Response(
			array(
				'ok'      => true,
				'orderId' => $order_id,
				'status'  => $status,
			),
			200
		);
	}

	/**
	 * Get normalized config.
	 *
	 * @return array<string,mixed>
	 */
	private function get_config(): array {
		$raw = get_option( self::OPTION_CONFIG, array() );
		return $this->normalize_config_payload( is_array( $raw ) ? $raw : array(), $this->get_default_config() );
	}

	/**
	 * Return admin-safe config without raw tokens.
	 *
	 * @param array<string,mixed>|null $config Config override.
	 * @return array<string,mixed>
	 */
	private function get_public_admin_config( ?array $config = null ): array {
		$config = $config ? $this->normalize_config_payload( $config, $this->get_default_config() ) : $this->get_config();

		$config['catalog']['hasToken'] = '' !== (string) ( $config['catalog']['tokenHash'] ?? '' );
		unset( $config['catalog']['tokenHash'] );

		foreach ( $config['tables'] as $table_key => $table ) {
			$config['tables'][ $table_key ]['hasToken'] = '' !== (string) ( $table['tokenHash'] ?? '' );
			unset( $config['tables'][ $table_key ]['tokenHash'] );
		}

		return $config;
	}

	/**
	 * Default config shape.
	 *
	 * @return array<string,mixed>
	 */
	private function get_default_config(): array {
		return array(
			'version'   => 1,
			'mode'      => 'disabled',
			'pageUrl'   => '',
			'catalog'   => array(
				'enabled'   => false,
				'tokenHash' => '',
				'createdAt' => '',
				'updatedAt' => '',
			),
			'tables'    => array(),
			'updatedAt' => '',
		);
	}

	/**
	 * Normalize config payload.
	 *
	 * @param array<string,mixed> $payload Incoming payload.
	 * @param array<string,mixed> $current Current config.
	 * @return array<string,mixed>
	 */
	private function normalize_config_payload( array $payload, array $current ): array {
		$now     = gmdate( 'c' );
		$current = array_replace_recursive( $this->get_default_config(), $current );
		$catalog_payload = array_key_exists( 'catalog', $payload ) && is_array( $payload['catalog'] ) ? $payload['catalog'] : ( isset( $current['catalog'] ) && is_array( $current['catalog'] ) ? $current['catalog'] : array() );
		$tables_payload  = array_key_exists( 'tables', $payload ) && is_array( $payload['tables'] ) ? $payload['tables'] : ( isset( $current['tables'] ) && is_array( $current['tables'] ) ? $current['tables'] : array() );
		$mode    = sanitize_key( (string) ( $payload['mode'] ?? $current['mode'] ?? 'disabled' ) );
		if ( ! in_array( $mode, array( 'disabled', 'online', 'catalog' ), true ) ) {
			$mode = 'disabled';
		}

		$config = array(
			'version'   => 1,
			'mode'      => $mode,
			'pageUrl'   => $this->normalize_page_url( (string) ( $payload['pageUrl'] ?? $payload['page_url'] ?? $current['pageUrl'] ?? '' ) ),
			'catalog'   => $this->normalize_catalog_config( $catalog_payload, isset( $current['catalog'] ) && is_array( $current['catalog'] ) ? $current['catalog'] : array(), $now ),
			'tables'    => $this->normalize_table_configs( $tables_payload, isset( $current['tables'] ) && is_array( $current['tables'] ) ? $current['tables'] : array(), $now ),
			'updatedAt' => $now,
		);

		if ( 'catalog' !== $mode ) {
			$config['catalog']['enabled']   = false;
			$config['catalog']['tokenHash'] = '';
		}

		if ( 'online' !== $mode ) {
			$config['tables'] = array();
		}

		return $config;
	}

	/**
	 * Normalize catalog config.
	 *
	 * @param array<string,mixed> $payload Incoming catalog payload.
	 * @param array<string,mixed> $current Current catalog config.
	 * @param string              $now     Current ISO timestamp.
	 * @return array<string,mixed>
	 */
	private function normalize_catalog_config( array $payload, array $current, string $now ): array {
		$has_token_payload = array_key_exists( 'token', $payload );
		$token_hash        = $has_token_payload ? $this->hash_token( (string) $payload['token'] ) : '';
		if ( '' === $token_hash && ! $has_token_payload ) {
			$token_hash = sanitize_text_field( (string) ( $payload['tokenHash'] ?? $current['tokenHash'] ?? '' ) );
		}

		$created_at = sanitize_text_field( (string) ( $current['createdAt'] ?? '' ) );
		if ( '' === $created_at && '' !== $token_hash ) {
			$created_at = $now;
		}

		return array(
			'enabled'   => array_key_exists( 'enabled', $payload ) ? (bool) $payload['enabled'] : (bool) ( $current['enabled'] ?? false ),
			'tokenHash' => $token_hash,
			'createdAt' => $created_at,
			'updatedAt' => $now,
		);
	}

	/**
	 * Normalize table configs.
	 *
	 * @param array<string|int,mixed> $payload Incoming table payload.
	 * @param array<string,mixed>     $current Current table configs.
	 * @param string                  $now     Current ISO timestamp.
	 * @return array<string,array<string,mixed>>
	 */
	private function normalize_table_configs( array $payload, array $current, string $now ): array {
		$tables = array();

		foreach ( $payload as $raw_key => $raw_table ) {
			if ( ! is_array( $raw_table ) ) {
				continue;
			}

			$table_key = sanitize_text_field( (string) ( $raw_table['tableKey'] ?? $raw_table['table_key'] ?? ( is_string( $raw_key ) ? $raw_key : '' ) ) );
			if ( '' === $table_key ) {
				continue;
			}

			$previous          = isset( $current[ $table_key ] ) && is_array( $current[ $table_key ] ) ? $current[ $table_key ] : array();
			$has_token_payload = array_key_exists( 'token', $raw_table );
			$token_hash        = $has_token_payload ? $this->hash_token( (string) $raw_table['token'] ) : '';
			if ( '' === $token_hash && ! $has_token_payload ) {
				$token_hash = sanitize_text_field( (string) ( $raw_table['tokenHash'] ?? $previous['tokenHash'] ?? '' ) );
			}

			$created_at = sanitize_text_field( (string) ( $previous['createdAt'] ?? '' ) );
			if ( '' === $created_at && '' !== $token_hash ) {
				$created_at = $now;
			}

			$tables[ $table_key ] = array(
				'tableKey'    => $table_key,
				'tableId'     => sanitize_text_field( (string) ( $raw_table['tableId'] ?? $raw_table['table_id'] ?? $previous['tableId'] ?? '' ) ),
				'label'       => sanitize_text_field( (string) ( $raw_table['label'] ?? $raw_table['tableLabel'] ?? $raw_table['table_label'] ?? $previous['label'] ?? '' ) ),
				'zoneId'      => sanitize_text_field( (string) ( $raw_table['zoneId'] ?? $raw_table['zone_id'] ?? $previous['zoneId'] ?? '' ) ),
				'zoneLabel'   => sanitize_text_field( (string) ( $raw_table['zoneLabel'] ?? $raw_table['zone_label'] ?? $previous['zoneLabel'] ?? '' ) ),
				'tableNumber' => max( 0, absint( $raw_table['tableNumber'] ?? $raw_table['table_number'] ?? $previous['tableNumber'] ?? 0 ) ),
				'enabled'     => array_key_exists( 'enabled', $raw_table ) ? (bool) $raw_table['enabled'] : (bool) ( $previous['enabled'] ?? true ),
				'tokenHash'   => $token_hash,
				'createdAt'   => $created_at,
				'updatedAt'   => $now,
			);
		}

		return $tables;
	}

	/**
	 * Normalize page URL.
	 *
	 * @param string $raw Raw URL.
	 * @return string
	 */
	private function normalize_page_url( string $raw ): string {
		$raw = trim( $raw );
		if ( '' === $raw ) {
			return '';
		}

		$url    = esc_url_raw( $raw );
		$scheme = is_string( $url ) ? wp_parse_url( $url, PHP_URL_SCHEME ) : '';
		if ( ! in_array( $scheme, array( 'http', 'https' ), true ) ) {
			return '';
		}

		return untrailingslashit( $url );
	}

	/**
	 * Read token from request.
	 *
	 * @param WP_REST_Request $request Request object.
	 * @return string
	 */
	private function read_token_from_request( WP_REST_Request $request ): string {
		$token = $request->get_param( self::TOKEN_PARAM );
		if ( ! is_string( $token ) || '' === trim( $token ) ) {
			$token = $request->get_param( 'token' );
		}

		return is_string( $token ) ? trim( $token ) : '';
	}

	/**
	 * Resolve a token to catalog/table config.
	 *
	 * @param string $token Raw token.
	 * @return array<string,mixed>|WP_Error
	 */
	private function resolve_token_record( string $token ) {
		$hash = $this->hash_token( $token );
		if ( '' === $hash ) {
			return new WP_Error(
				'foodord_table_ordering_invalid_token',
				__( 'Invalid table ordering token.', 'foodord' ),
				array( 'status' => 404 )
			);
		}

		$config = $this->get_config();
		$mode   = (string) ( $config['mode'] ?? 'disabled' );
		if ( 'disabled' === $mode ) {
			return new WP_Error(
				'foodord_table_ordering_disabled',
				__( 'Table ordering is disabled.', 'foodord' ),
				array( 'status' => 403 )
			);
		}

		if ( 'catalog' === $mode ) {
			$catalog = isset( $config['catalog'] ) && is_array( $config['catalog'] ) ? $config['catalog'] : array();
			if ( ! empty( $catalog['enabled'] ) && hash_equals( (string) ( $catalog['tokenHash'] ?? '' ), $hash ) ) {
				return array(
					'mode'      => 'catalog',
					'tokenHash' => $hash,
				);
			}
		}

		if ( 'online' === $mode ) {
			$tables = isset( $config['tables'] ) && is_array( $config['tables'] ) ? $config['tables'] : array();
			foreach ( $tables as $table ) {
				if ( ! is_array( $table ) || empty( $table['enabled'] ) ) {
					continue;
				}

				if ( hash_equals( (string) ( $table['tokenHash'] ?? '' ), $hash ) ) {
					return array(
						'mode'      => 'online',
						'tokenHash' => $hash,
						'table'     => $this->public_table_payload( $table ),
					);
				}
			}
		}

		return new WP_Error(
			'foodord_table_ordering_invalid_token',
			__( 'Invalid table ordering token.', 'foodord' ),
			array( 'status' => 404 )
		);
	}

	/**
	 * Public table payload.
	 *
	 * @param array<string,mixed> $table Table config.
	 * @return array<string,mixed>
	 */
	private function public_table_payload( array $table ): array {
		return array(
			'tableKey'    => sanitize_text_field( (string) ( $table['tableKey'] ?? '' ) ),
			'tableId'     => sanitize_text_field( (string) ( $table['tableId'] ?? '' ) ),
			'label'       => sanitize_text_field( (string) ( $table['label'] ?? '' ) ),
			'zoneId'      => sanitize_text_field( (string) ( $table['zoneId'] ?? '' ) ),
			'zoneLabel'   => sanitize_text_field( (string) ( $table['zoneLabel'] ?? '' ) ),
			'tableNumber' => max( 0, absint( $table['tableNumber'] ?? 0 ) ),
		);
	}

	/**
	 * Hash a raw token.
	 *
	 * @param string $token Raw token.
	 * @return string
	 */
	private function hash_token( string $token ): string {
		$token = trim( $token );
		if ( strlen( $token ) < 16 ) {
			return '';
		}

		return hash( 'sha256', $token );
	}

	/**
	 * Normalize order items.
	 *
	 * @param mixed $raw_items Raw items.
	 * @return array<int,array<string,mixed>>|WP_Error
	 */
	private function normalize_order_items( $raw_items ) {
		if ( ! is_array( $raw_items ) ) {
			return array();
		}

		$items = array();
		foreach ( array_slice( $raw_items, 0, 100 ) as $raw_item ) {
			if ( ! is_array( $raw_item ) ) {
				continue;
			}

			$item = $this->normalize_order_item( $raw_item );
			if ( is_wp_error( $item ) ) {
				return $item;
			}

			if ( $item ) {
				$items[] = $item;
			}
		}

		return $items;
	}

	/**
	 * Normalize the current WooCommerce cart into a table order payload.
	 *
	 * @return array<int,array<string,mixed>>|WP_Error
	 */
	private function normalize_cart_order_items() {
		$this->ensure_cart_loaded();
		if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
			return array();
		}

		$items = array();
		foreach ( WC()->cart->get_cart() as $cart_item ) {
			if ( ! is_array( $cart_item ) ) {
				continue;
			}

			$item = $this->normalize_cart_order_item( $cart_item );
			if ( is_wp_error( $item ) ) {
				return $item;
			}

			if ( $item ) {
				$items[] = $item;
			}
		}

		return $items;
	}

	/**
	 * Ensure WooCommerce cart is available during REST requests.
	 */
	private function ensure_cart_loaded(): void {
		if ( function_exists( 'WC' ) && WC()->cart ) {
			return;
		}

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

	/**
	 * Normalize a WooCommerce cart item.
	 *
	 * @param array<string,mixed> $cart_item Cart item.
	 * @return array<string,mixed>|null|WP_Error
	 */
	private function normalize_cart_order_item( array $cart_item ) {
		if ( ! function_exists( 'wc_get_product' ) ) {
			return new WP_Error(
				'foodord_table_ordering_woocommerce_inactive',
				__( 'WooCommerce is not active.', 'foodord' ),
				array( 'status' => 500 )
			);
		}

		$product = isset( $cart_item['data'] ) && $cart_item['data'] instanceof WC_Product ? $cart_item['data'] : null;
		if ( ! $product ) {
			$product = wc_get_product( isset( $cart_item['variation_id'] ) && $cart_item['variation_id'] ? (int) $cart_item['variation_id'] : (int) ( $cart_item['product_id'] ?? 0 ) );
		}

		if ( ! $product instanceof WC_Product ) {
			return null;
		}

		$product_id               = isset( $cart_item['product_id'] ) ? absint( $cart_item['product_id'] ) : (int) $product->get_id();
		$variation_id             = isset( $cart_item['variation_id'] ) ? absint( $cart_item['variation_id'] ) : 0;
		$parent_product           = $product_id > 0 ? wc_get_product( $product_id ) : null;
		$name_source              = $parent_product instanceof WC_Product ? $parent_product : $product;
		$quantity                 = max( 1, min( 99, absint( $cart_item['quantity'] ?? 1 ) ) );
		$inherited_toppings_extra = isset( $cart_item['foodord_toppings_extra'] ) ? max( 0, (float) $cart_item['foodord_toppings_extra'] ) : 0.0;
		$toppings_extra           = $inherited_toppings_extra;
		$tax_fees                 = isset( $cart_item['foodord_topping_tax_fees'] ) && is_array( $cart_item['foodord_topping_tax_fees'] ) ? $cart_item['foodord_topping_tax_fees'] : array();
		foreach ( $tax_fees as $tax_fee ) {
			if ( is_array( $tax_fee ) ) {
				$toppings_extra += max( 0, (float) ( $tax_fee['price'] ?? 0 ) );
			}
		}
		$base_price = isset( $cart_item['foodord_toppings_base_price'] ) ? max( 0, (float) $cart_item['foodord_toppings_base_price'] ) : max( 0, (float) $product->get_price( 'edit' ) - $inherited_toppings_extra );
		$note       = isset( $cart_item['foodord_note'] ) ? sanitize_textarea_field( (string) $cart_item['foodord_note'] ) : '';

		if ( function_exists( 'mb_substr' ) ) {
			$note = mb_substr( $note, 0, 160 );
		} else {
			$note = substr( $note, 0, 160 );
		}

		return array(
			'productId'      => (string) $product_id,
			'variationId'    => $variation_id > 0 ? (string) $variation_id : '',
			'productName'    => sanitize_text_field( $name_source->get_name() ),
			'name'           => sanitize_text_field( $name_source->get_name() ),
			'variationLabel' => $variation_id > 0 && $product instanceof WC_Product_Variation ? $this->build_variation_label( $product ) : '',
			'sku'            => sanitize_text_field( (string) $product->get_sku() ),
			'unitPrice'      => $base_price,
			'currency'       => function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : '',
			'quantity'       => $quantity,
			'toppings'       => $this->normalize_cart_toppings_for_order( $cart_item['foodord_toppings'] ?? array() ),
			'toppingsExtra'  => $toppings_extra,
			'modifiers'      => array(),
			'modifiersExtra' => 0,
			'note'           => $note,
		);
	}

	/**
	 * Normalize cart toppings for the desktop POS payload.
	 *
	 * @param mixed $raw_toppings Raw cart toppings.
	 * @return array<int,array<string,mixed>>
	 */
	private function normalize_cart_toppings_for_order( $raw_toppings ): array {
		if ( empty( $raw_toppings ) || ! is_array( $raw_toppings ) ) {
			return array();
		}

		$sets = array();
		foreach ( $raw_toppings as $set ) {
			if ( ! is_array( $set ) ) {
				continue;
			}

			$items = array();
			foreach ( isset( $set['items'] ) && is_array( $set['items'] ) ? $set['items'] : array() as $item ) {
				if ( ! is_array( $item ) ) {
					continue;
				}

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

				$items[] = array(
					'id'    => sanitize_text_field( (string) ( $item['id'] ?? '' ) ),
					'label' => $label,
					'price' => max( 0, (float) ( $item['price'] ?? 0 ) ),
				);
			}

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

			$sets[] = array(
				'set_id'     => sanitize_text_field( (string) ( $set['set_id'] ?? '' ) ),
				'set_name'   => sanitize_text_field( (string) ( $set['set_name'] ?? '' ) ),
				'set_label'  => sanitize_text_field( (string) ( $set['set_label'] ?? '' ) ),
				'group_type' => sanitize_key( (string) ( $set['group_type'] ?? '' ) ),
				'items'      => $items,
			);
		}

		return $sets;
	}

	/**
	 * Clear WooCommerce cart after a table order was queued.
	 */
	private function clear_customer_cart(): void {
		$this->ensure_cart_loaded();
		if ( function_exists( 'WC' ) && WC()->cart ) {
			WC()->cart->empty_cart();
		}
	}

	/**
	 * Normalize one order item.
	 *
	 * @param array<string,mixed> $raw_item Raw item.
	 * @return array<string,mixed>|null|WP_Error
	 */
	private function normalize_order_item( array $raw_item ) {
		if ( ! function_exists( 'wc_get_product' ) ) {
			return new WP_Error(
				'foodord_table_ordering_woocommerce_inactive',
				__( 'WooCommerce is not active.', 'foodord' ),
				array( 'status' => 500 )
			);
		}

		$product_id   = absint( $raw_item['productId'] ?? $raw_item['product_id'] ?? 0 );
		$variation_id = absint( $raw_item['variationId'] ?? $raw_item['variation_id'] ?? 0 );
		$product      = $variation_id > 0 ? wc_get_product( $variation_id ) : wc_get_product( $product_id );

		if ( ! $product instanceof WC_Product ) {
			return null;
		}

		$parent_id = $product instanceof WC_Product_Variation ? (int) $product->get_parent_id() : (int) $product->get_id();
		if ( $product_id > 0 && $parent_id > 0 && $product_id !== $parent_id && $variation_id > 0 ) {
			return new WP_Error(
				'foodord_table_ordering_invalid_variation',
				__( 'Invalid product variation.', 'foodord' ),
				array( 'status' => 400 )
			);
		}

		if ( class_exists( 'Foodord_Product_Availability' ) ) {
			if ( $parent_id > 0 && ! Foodord_Product_Availability::is_available( $parent_id ) ) {
				return new WP_Error(
					'foodord_table_ordering_product_unavailable',
					__( 'A selected product is currently unavailable.', 'foodord' ),
					array( 'status' => 400 )
				);
			}

			if ( $variation_id > 0 && ! Foodord_Product_Availability::is_available( $variation_id ) ) {
				return new WP_Error(
					'foodord_table_ordering_product_unavailable',
					__( 'A selected product is currently unavailable.', 'foodord' ),
					array( 'status' => 400 )
				);
			}
		}

		$parent_product = $parent_id > 0 ? wc_get_product( $parent_id ) : null;
		$name_source    = $parent_product instanceof WC_Product ? $parent_product : $product;
		$quantity       = max( 1, min( 99, absint( $raw_item['quantity'] ?? 1 ) ) );
		$toppings       = $this->normalize_item_toppings( $parent_id > 0 ? $parent_id : (int) $product->get_id(), $raw_item );
		if ( is_wp_error( $toppings ) ) {
			return $toppings;
		}

		$note = sanitize_textarea_field( (string) ( $raw_item['note'] ?? '' ) );
		if ( function_exists( 'mb_substr' ) ) {
			$note = mb_substr( $note, 0, 160 );
		} else {
			$note = substr( $note, 0, 160 );
		}

		return array(
			'productId'      => (string) ( $parent_id > 0 ? $parent_id : $product->get_id() ),
			'variationId'    => $variation_id > 0 ? (string) $variation_id : '',
			'productName'    => sanitize_text_field( $name_source->get_name() ),
			'name'           => sanitize_text_field( $name_source->get_name() ),
			'variationLabel' => $variation_id > 0 && $product instanceof WC_Product_Variation ? $this->build_variation_label( $product ) : '',
			'sku'            => sanitize_text_field( (string) $product->get_sku() ),
			'unitPrice'      => max( 0, (float) $product->get_price( 'edit' ) ),
			'currency'       => function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : '',
			'quantity'       => $quantity,
			'toppings'       => $toppings['sets'],
			'toppingsExtra'  => $toppings['extra'],
			'modifiers'      => array(),
			'modifiersExtra' => 0,
			'note'           => $note,
		);
	}

	/**
	 * Normalize toppings against configured FoodOrd topping sets.
	 *
	 * @param int                 $product_id Product ID.
	 * @param array<string,mixed> $raw_item   Raw item.
	 * @return array{sets:array<int,array<string,mixed>>,extra:float}|WP_Error
	 */
	private function normalize_item_toppings( int $product_id, array $raw_item ) {
		$raw_payload = $this->extract_toppings_payload( $raw_item );
		if ( empty( $raw_payload ) ) {
			return array(
				'sets'  => array(),
				'extra' => 0.0,
			);
		}

		if ( ! class_exists( 'Foodord_Product_Modal' ) ) {
			return array(
				'sets'  => array(),
				'extra' => 0.0,
			);
		}

		$modal = new Foodord_Product_Modal();
		$sets  = method_exists( $modal, 'get_product_topping_sets_for_api' ) ? $modal->get_product_topping_sets_for_api( $product_id ) : array();
		if ( empty( $sets ) || ! is_array( $sets ) ) {
			return array(
				'sets'  => array(),
				'extra' => 0.0,
			);
		}

		$result = array();
		$extra  = 0.0;
		foreach ( $sets as $set ) {
			if ( ! is_array( $set ) || empty( $set['id'] ) ) {
				continue;
			}

			$set_id = sanitize_text_field( (string) $set['id'] );
			if ( ! array_key_exists( $set_id, $raw_payload ) ) {
				if ( ! empty( $set['is_required'] ) ) {
					return new WP_Error(
						'foodord_table_ordering_required_topping_missing',
						__( 'Please complete the required selections.', 'foodord' ),
						array( 'status' => 400 )
					);
				}
				continue;
			}

			$selected_ids = $this->normalize_selected_topping_ids( $raw_payload[ $set_id ] );
			$item_lookup  = array();
			foreach ( isset( $set['items'] ) && is_array( $set['items'] ) ? $set['items'] : array() as $item ) {
				if ( is_array( $item ) && ! empty( $item['id'] ) ) {
					$item_lookup[ (string) $item['id'] ] = $item;
				}
			}

			$selected_items = array();
			foreach ( $selected_ids as $selected_id ) {
				if ( ! isset( $item_lookup[ $selected_id ] ) ) {
					continue;
				}

				$item  = $item_lookup[ $selected_id ];
				$price = max( 0, (float) ( $item['price'] ?? 0 ) );
				$extra += $price;
				$selected_items[] = array(
					'id'    => $selected_id,
					'label' => sanitize_text_field( (string) ( $item['label'] ?? '' ) ),
					'price' => $price,
				);
			}

			$count = count( $selected_items );
			if ( ! $count && ! empty( $set['is_required'] ) ) {
				return new WP_Error(
					'foodord_table_ordering_required_topping_missing',
					__( 'Please complete the required selections.', 'foodord' ),
					array( 'status' => 400 )
				);
			}

			$group_type = sanitize_key( (string) ( $set['group_type'] ?? '' ) );
			if ( 'checkbox' === $group_type ) {
				$min = isset( $set['checkbox_min'] ) ? absint( $set['checkbox_min'] ) : 0;
				$max = isset( $set['checkbox_max'] ) ? absint( $set['checkbox_max'] ) : 0;
				if ( $min > 0 && $count < $min ) {
					return new WP_Error(
						'foodord_table_ordering_topping_minimum',
						__( 'Please complete the required selections.', 'foodord' ),
						array( 'status' => 400 )
					);
				}
				if ( $max > 0 && $count > $max ) {
					return new WP_Error(
						'foodord_table_ordering_topping_maximum',
						__( 'Too many topping selections.', 'foodord' ),
						array( 'status' => 400 )
					);
				}
			} elseif ( $count > 1 ) {
				$selected_items = array_slice( $selected_items, 0, 1 );
			}

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

			$result[] = array(
				'set_id'     => $set_id,
				'set_name'   => sanitize_text_field( (string) ( $set['name'] ?? '' ) ),
				'set_label'  => sanitize_text_field( (string) ( $set['label'] ?? '' ) ),
				'group_type' => $group_type,
				'items'      => $selected_items,
			);
		}

		return array(
			'sets'  => $result,
			'extra' => $extra,
		);
	}

	/**
	 * Extract topping selections from supported payload shapes.
	 *
	 * @param array<string,mixed> $raw_item Raw item.
	 * @return array<string,mixed>
	 */
	private function extract_toppings_payload( array $raw_item ): array {
		$payload = $raw_item['toppingsPayload'] ?? $raw_item['toppings_payload'] ?? null;
		if ( is_array( $payload ) ) {
			return $payload;
		}

		$raw_toppings = isset( $raw_item['toppings'] ) && is_array( $raw_item['toppings'] ) ? $raw_item['toppings'] : array();
		$mapped       = array();
		foreach ( $raw_toppings as $set ) {
			if ( ! is_array( $set ) || empty( $set['set_id'] ) ) {
				continue;
			}

			$set_id = sanitize_text_field( (string) $set['set_id'] );
			$items  = isset( $set['items'] ) && is_array( $set['items'] ) ? $set['items'] : array();
			$ids    = array();
			foreach ( $items as $item ) {
				if ( is_array( $item ) && isset( $item['id'] ) ) {
					$ids[] = sanitize_text_field( (string) $item['id'] );
				} elseif ( is_string( $item ) ) {
					$ids[] = sanitize_text_field( $item );
				}
			}

			$mapped[ $set_id ] = $ids;
		}

		return $mapped;
	}

	/**
	 * Normalize selected topping ids.
	 *
	 * @param mixed $value Raw selected value.
	 * @return string[]
	 */
	private function normalize_selected_topping_ids( $value ): array {
		$values = is_array( $value ) ? $value : array( $value );
		$ids    = array();
		foreach ( $values as $item ) {
			if ( is_array( $item ) && isset( $item['id'] ) ) {
				$item = $item['id'];
			}

			$id = sanitize_text_field( (string) $item );
			if ( '' !== $id ) {
				$ids[] = $id;
			}
		}

		return array_values( array_unique( $ids ) );
	}

	/**
	 * Build a human-readable variation label.
	 *
	 * @param WC_Product_Variation $variation Variation product.
	 * @return string
	 */
	private function build_variation_label( WC_Product_Variation $variation ): string {
		$parts = array();
		foreach ( $variation->get_attributes() as $taxonomy => $value ) {
			$value = (string) $value;
			if ( '' === $value ) {
				continue;
			}

			$label = $value;
			if ( taxonomy_exists( $taxonomy ) ) {
				$term = get_term_by( 'slug', $value, $taxonomy );
				if ( $term instanceof WP_Term ) {
					$label = $term->name;
				}
			}

			$parts[] = sanitize_text_field( $label );
		}

		return implode( ' / ', array_filter( $parts ) );
	}

	/**
	 * Calculate order total.
	 *
	 * @param array<int,array<string,mixed>> $items Normalized items.
	 * @return float
	 */
	private function calculate_order_total( array $items ): float {
		$total = 0.0;
		foreach ( $items as $item ) {
			$quantity = max( 1, absint( $item['quantity'] ?? 1 ) );
			$unit     = max( 0, (float) ( $item['unitPrice'] ?? 0 ) );
			$extra    = max( 0, (float) ( $item['toppingsExtra'] ?? 0 ) );
			$total   += ( $unit + $extra ) * $quantity;
		}

		return round( $total, 2 );
	}

	/**
	 * Get orders store.
	 *
	 * @return array<string,mixed>
	 */
	private function get_orders_store(): array {
		$raw = get_option( self::OPTION_ORDERS, array() );
		if ( ! is_array( $raw ) ) {
			$raw = array();
		}

		$raw['version'] = 1;
		$raw['orders']  = isset( $raw['orders'] ) && is_array( $raw['orders'] ) ? array_values( $raw['orders'] ) : array();

		return $this->prune_orders_store( $raw );
	}

	/**
	 * Prune old queued orders.
	 *
	 * @param array<string,mixed> $store Orders store.
	 * @return array<string,mixed>
	 */
	private function prune_orders_store( array $store ): array {
		$orders = isset( $store['orders'] ) && is_array( $store['orders'] ) ? $store['orders'] : array();
		$cutoff = time() - ( self::ORDER_RETENTION_DAYS * DAY_IN_SECONDS );

		$orders = array_values(
			array_filter(
				$orders,
				static function ( $order ) use ( $cutoff ) {
					if ( ! is_array( $order ) ) {
						return false;
					}

					$status     = (string) ( $order['status'] ?? '' );
					$updated_at = strtotime( (string) ( $order['updatedAt'] ?? $order['createdAt'] ?? '' ) );
					if ( ! $updated_at ) {
						return true;
					}

					if ( 'pending' === $status ) {
						return $updated_at >= $cutoff;
					}

					return $updated_at >= $cutoff;
				}
			)
		);

		usort(
			$orders,
			static function ( $left, $right ) {
				$left_time  = strtotime( (string) ( $left['createdAt'] ?? '' ) );
				$right_time = strtotime( (string) ( $right['createdAt'] ?? '' ) );
				$left_time  = false !== $left_time ? $left_time : 0;
				$right_time = false !== $right_time ? $right_time : 0;
				return $right_time <=> $left_time;
			}
		);

		$store['version'] = 1;
		$store['orders']  = array_slice( $orders, 0, self::MAX_STORED_ORDERS );

		return $store;
	}

	/**
	 * Find an existing idempotent order.
	 *
	 * @param array<string,mixed> $store           Orders store.
	 * @param string              $token_hash      Token hash.
	 * @param string              $client_order_id Client order ID.
	 * @return array<string,mixed>|null
	 */
	private function find_existing_order( array $store, string $token_hash, string $client_order_id ): ?array {
		if ( '' === $token_hash || '' === $client_order_id ) {
			return null;
		}

		foreach ( isset( $store['orders'] ) && is_array( $store['orders'] ) ? $store['orders'] : array() as $order ) {
			if ( ! is_array( $order ) ) {
				continue;
			}

			if ( (string) ( $order['tokenHash'] ?? '' ) === $token_hash && (string) ( $order['clientOrderId'] ?? '' ) === $client_order_id ) {
				return $order;
			}
		}

		return null;
	}

	/**
	 * Strip private fields from orders.
	 *
	 * @param array<int,array<string,mixed>> $orders Orders.
	 * @return array<int,array<string,mixed>>
	 */
	private function strip_private_order_fields( array $orders ): array {
		return array_map(
			static function ( array $order ): array {
				unset( $order['tokenHash'] );
				return $order;
			},
			$orders
		);
	}

	/**
	 * Generate order ID.
	 *
	 * @return string
	 */
	private function generate_order_id(): string {
		try {
			$random = bin2hex( random_bytes( 6 ) );
		} catch ( Exception $e ) {
			$random = wp_generate_password( 12, false, false );
		}

		return 'TO-' . gmdate( 'YmdHis' ) . '-' . $random;
	}
}
