<?php
/**
 * Payment Helper - Global payment utilities
 *
 * @package SureDonation
 */

namespace SureDonation\Inc\Payments;

use SureDonation\Inc\Helper;
use WP_Error;

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Payment_Helper class
 * Provides gateway-agnostic payment utilities
 *
 * @since 0.0.1
 */
class Payment_Helper {
	/**
	 * Option key for payment settings within consolidated options.
	 *
	 * @since 0.0.1
	 */
	public const OPTION_KEY = 'payment_settings';

	/**
	 * Get all payment settings
	 *
	 * @return array<string, mixed> Payment settings.
	 * @since 0.0.1
	 */
	public static function get_all_payment_settings() {
		$options = Helper::get_suredonation_option( self::OPTION_KEY, [] );

		// Ensure default structure.
		$defaults = [
			'currency'     => 'USD',
			'payment_mode' => 'test', // Valid values: test or live.
			'stripe'       => [],
		];

		$options_array = is_array( $options ) ? $options : [];
		return wp_parse_args( $options_array, $defaults );
	}

	/**
	 * Update all payment settings
	 *
	 * @param array<string, mixed> $settings Payment settings.
	 * @return bool True on success.
	 * @since 0.0.1
	 */
	public static function update_all_payment_settings( $settings ) {
		return Helper::update_suredonation_option( self::OPTION_KEY, $settings );
	}

	/**
	 * Get gateway-specific settings
	 *
	 * @param string $gateway Gateway name (e.g., 'stripe').
	 * @return array<string, mixed> Gateway settings.
	 * @since 0.0.1
	 */
	public static function get_gateway_settings( $gateway ) {
		$all_settings     = self::get_all_payment_settings();
		$gateway_settings = $all_settings[ $gateway ] ?? [];
		return is_array( $gateway_settings ) ? $gateway_settings : [];
	}

	/**
	 * Update gateway-specific settings
	 *
	 * @param string               $gateway  Gateway name.
	 * @param array<string, mixed> $settings Gateway settings.
	 * @return bool True on success.
	 * @since 0.0.1
	 */
	public static function update_gateway_settings( $gateway, $settings ) {
		$all_settings             = self::get_all_payment_settings();
		$all_settings[ $gateway ] = $settings;
		return self::update_all_payment_settings( $all_settings );
	}

	/**
	 * Get global payment setting
	 *
	 * @param string $key           Setting key.
	 * @param mixed  $default_value Default value.
	 * @return mixed Setting value.
	 * @since 0.0.1
	 */
	public static function get_global_setting( $key, $default_value = '' ) {
		$all_settings = self::get_all_payment_settings();
		return $all_settings[ $key ] ?? $default_value;
	}

	/**
	 * Update global payment setting
	 *
	 * @param string $key   Setting key.
	 * @param mixed  $value Setting value.
	 * @return bool True on success.
	 * @since 0.0.1
	 */
	public static function update_global_setting( $key, $value ) {
		$all_settings         = self::get_all_payment_settings();
		$all_settings[ $key ] = $value;
		return self::update_all_payment_settings( $all_settings );
	}

	/**
	 * Get current payment mode
	 *
	 * @return string 'test' or 'live'.
	 * @since 0.0.1
	 */
	public static function get_payment_mode() {
		$response = self::get_global_setting( 'payment_mode', 'test' );
		return ! empty( $response ) && is_string( $response ) ? $response : 'test';
	}

	/**
	 * Get comprehensive currency data for all supported currencies.
	 *
	 * This is the single source of truth for all currency-related data.
	 * Contains currency name, symbol, and decimal places.
	 *
	 * @return array<string, array<string, mixed>> Array of currency data keyed by currency code.
	 * @since 0.0.1
	 */
	public static function get_all_currencies_data() {
		return [
			'USD' => [
				'name'           => __( 'US Dollar', 'suredonation' ),
				'symbol'         => '$',
				'decimal_places' => 2,
			],
			'EUR' => [
				'name'           => __( 'Euro', 'suredonation' ),
				'symbol'         => '€',
				'decimal_places' => 2,
			],
			'GBP' => [
				'name'           => __( 'British Pound', 'suredonation' ),
				'symbol'         => '£',
				'decimal_places' => 2,
			],
			'JPY' => [
				'name'           => __( 'Japanese Yen', 'suredonation' ),
				'symbol'         => '¥',
				'decimal_places' => 0,
			],
			'AUD' => [
				'name'           => __( 'Australian Dollar', 'suredonation' ),
				'symbol'         => 'A$',
				'decimal_places' => 2,
			],
			'CAD' => [
				'name'           => __( 'Canadian Dollar', 'suredonation' ),
				'symbol'         => 'C$',
				'decimal_places' => 2,
			],
			'CHF' => [
				'name'           => __( 'Swiss Franc', 'suredonation' ),
				'symbol'         => 'CHF',
				'decimal_places' => 2,
			],
			'CNY' => [
				'name'           => __( 'Chinese Yuan', 'suredonation' ),
				'symbol'         => '¥',
				'decimal_places' => 2,
			],
			'SEK' => [
				'name'           => __( 'Swedish Krona', 'suredonation' ),
				'symbol'         => 'kr',
				'decimal_places' => 2,
			],
			'NZD' => [
				'name'           => __( 'New Zealand Dollar', 'suredonation' ),
				'symbol'         => 'NZ$',
				'decimal_places' => 2,
			],
			'MXN' => [
				'name'           => __( 'Mexican Peso', 'suredonation' ),
				'symbol'         => 'MX$',
				'decimal_places' => 2,
			],
			'SGD' => [
				'name'           => __( 'Singapore Dollar', 'suredonation' ),
				'symbol'         => 'S$',
				'decimal_places' => 2,
			],
			'HKD' => [
				'name'           => __( 'Hong Kong Dollar', 'suredonation' ),
				'symbol'         => 'HK$',
				'decimal_places' => 2,
			],
			'NOK' => [
				'name'           => __( 'Norwegian Krone', 'suredonation' ),
				'symbol'         => 'kr',
				'decimal_places' => 2,
			],
			'KRW' => [
				'name'           => __( 'South Korean Won', 'suredonation' ),
				'symbol'         => '₩',
				'decimal_places' => 0,
			],
			'TRY' => [
				'name'           => __( 'Turkish Lira', 'suredonation' ),
				'symbol'         => '₺',
				'decimal_places' => 2,
			],
			'RUB' => [
				'name'           => __( 'Russian Ruble', 'suredonation' ),
				'symbol'         => '₽',
				'decimal_places' => 2,
			],
			'INR' => [
				'name'           => __( 'Indian Rupee', 'suredonation' ),
				'symbol'         => '₹',
				'decimal_places' => 2,
			],
			'BRL' => [
				'name'           => __( 'Brazilian Real', 'suredonation' ),
				'symbol'         => 'R$',
				'decimal_places' => 2,
			],
			'ZAR' => [
				'name'           => __( 'South African Rand', 'suredonation' ),
				'symbol'         => 'R',
				'decimal_places' => 2,
			],
			'AED' => [
				'name'           => __( 'UAE Dirham', 'suredonation' ),
				'symbol'         => 'د.إ',
				'decimal_places' => 2,
			],
			'PHP' => [
				'name'           => __( 'Philippine Peso', 'suredonation' ),
				'symbol'         => '₱',
				'decimal_places' => 2,
			],
			'IDR' => [
				'name'           => __( 'Indonesian Rupiah', 'suredonation' ),
				'symbol'         => 'Rp',
				'decimal_places' => 2,
			],
			'MYR' => [
				'name'           => __( 'Malaysian Ringgit', 'suredonation' ),
				'symbol'         => 'RM',
				'decimal_places' => 2,
			],
			'THB' => [
				'name'           => __( 'Thai Baht', 'suredonation' ),
				'symbol'         => '฿',
				'decimal_places' => 2,
			],
			'BIF' => [
				'name'           => __( 'Burundian Franc', 'suredonation' ),
				'symbol'         => 'FBu',
				'decimal_places' => 0,
			],
			'CLP' => [
				'name'           => __( 'Chilean Peso', 'suredonation' ),
				'symbol'         => '$',
				'decimal_places' => 0,
			],
			'DJF' => [
				'name'           => __( 'Djiboutian Franc', 'suredonation' ),
				'symbol'         => 'Fdj',
				'decimal_places' => 0,
			],
			'GNF' => [
				'name'           => __( 'Guinean Franc', 'suredonation' ),
				'symbol'         => 'FG',
				'decimal_places' => 0,
			],
			'KMF' => [
				'name'           => __( 'Comorian Franc', 'suredonation' ),
				'symbol'         => 'CF',
				'decimal_places' => 0,
			],
			'MGA' => [
				'name'           => __( 'Malagasy Ariary', 'suredonation' ),
				'symbol'         => 'Ar',
				'decimal_places' => 0,
			],
			'PYG' => [
				'name'           => __( 'Paraguayan Guaraní', 'suredonation' ),
				'symbol'         => '₲',
				'decimal_places' => 0,
			],
			'RWF' => [
				'name'           => __( 'Rwandan Franc', 'suredonation' ),
				'symbol'         => 'FRw',
				'decimal_places' => 0,
			],
			'UGX' => [
				'name'           => __( 'Ugandan Shilling', 'suredonation' ),
				'symbol'         => 'USh',
				'decimal_places' => 0,
			],
			'VND' => [
				'name'           => __( 'Vietnamese Đồng', 'suredonation' ),
				'symbol'         => '₫',
				'decimal_places' => 0,
			],
			'VUV' => [
				'name'           => __( 'Vanuatu Vatu', 'suredonation' ),
				'symbol'         => 'VT',
				'decimal_places' => 0,
			],
			'XAF' => [
				'name'           => __( 'Central African CFA Franc', 'suredonation' ),
				'symbol'         => 'FCFA',
				'decimal_places' => 0,
			],
			'XOF' => [
				'name'           => __( 'West African CFA Franc', 'suredonation' ),
				'symbol'         => 'CFA',
				'decimal_places' => 0,
			],
			'XPF' => [
				'name'           => __( 'CFP Franc', 'suredonation' ),
				'symbol'         => '₣',
				'decimal_places' => 0,
			],
		];
	}

	/**
	 * Get currency names for all supported currencies.
	 *
	 * @return array<string, mixed> Array of currency names keyed by currency code.
	 * @since 0.0.1
	 */
	public static function get_currency_names() {
		$currencies = self::get_all_currencies_data();
		$names      = [];

		foreach ( $currencies as $code => $data ) {
			$names[ $code ] = $data['name'];
		}

		return $names;
	}

	/**
	 * Get currency
	 *
	 * @return string Currency code (e.g., 'USD').
	 * @since 0.0.1
	 */
	public static function get_currency() {
		$currency = self::get_global_setting( 'currency', 'USD' );
		return is_string( $currency ) ? $currency : 'USD';
	}

	/**
	 * Get currency symbol.
	 *
	 * @param string $currency Currency code.
	 * @return string Currency symbol or empty string.
	 * @since 0.0.1
	 */
	public static function get_currency_symbol( $currency = '' ) {
		if ( empty( $currency ) ) {
			$currency = self::get_currency();
		}

		if ( empty( $currency ) || ! is_string( $currency ) ) {
			return '';
		}

		$currency      = strtoupper( $currency );
		$currencies    = self::get_all_currencies_data();
		$currency_data = $currencies[ $currency ] ?? null;

		$symbol = ! empty( $currency_data ) ? $currency_data['symbol'] : '';
		return is_string( $symbol ) ? $symbol : '';
	}

	/**
	 * Get list of zero-decimal currencies.
	 *
	 * Zero-decimal currencies don't use decimal points in payment APIs.
	 * For these currencies, amounts are passed as-is without multiplying/dividing by 100.
	 *
	 * @return array<string> Array of zero-decimal currency codes.
	 * @since 0.0.1
	 */
	public static function get_zero_decimal_currencies() {
		$currencies         = self::get_all_currencies_data();
		$zero_decimal_codes = [];

		foreach ( $currencies as $code => $data ) {
			if ( 0 === $data['decimal_places'] ) {
				$zero_decimal_codes[] = $code;
			}
		}

		return $zero_decimal_codes;
	}

	/**
	 * Check if currency is zero-decimal.
	 *
	 * @param string $currency Currency code.
	 * @return bool True if zero-decimal currency.
	 * @since 0.0.1
	 */
	public static function is_zero_decimal_currency( $currency ) {
		if ( empty( $currency ) || ! is_string( $currency ) ) {
			return false;
		}

		$currency      = strtoupper( $currency );
		$currencies    = self::get_all_currencies_data();
		$currency_data = $currencies[ $currency ] ?? null;

		return $currency_data && 0 === $currency_data['decimal_places'];
	}

	/**
	 * Format amount for display
	 *
	 * @param float  $amount   Amount.
	 * @param string $currency Currency code.
	 * @return string Formatted amount.
	 * @since 0.0.1
	 */
	public static function format_amount( $amount, $currency = '' ) {
		if ( empty( $currency ) ) {
			$currency = self::get_currency();
		}

		$symbol         = self::get_currency_symbol( $currency );
		$decimal_places = self::is_zero_decimal_currency( $currency ) ? 0 : 2;

		return $symbol . number_format( (float) $amount, $decimal_places, '.', ',' );
	}

	/**
	 * Convert amount to Stripe format (cents)
	 *
	 * @param float  $amount   Amount in dollars.
	 * @param string $currency Currency code.
	 * @return int Amount in cents.
	 * @since 0.0.1
	 */
	public static function amount_to_stripe_format( $amount, $currency = '' ) {
		if ( empty( $currency ) ) {
			$currency = self::get_currency();
		}

		$amount = floatval( $amount );
		return self::is_zero_decimal_currency( $currency )
			? (int) round( $amount )
			: (int) round( $amount * 100 );
	}

	/**
	 * Convert amount from Stripe format (cents) to dollars
	 *
	 * @param int    $amount   Amount in cents.
	 * @param string $currency Currency code.
	 * @return float Amount in dollars.
	 * @since 0.0.1
	 */
	public static function amount_from_stripe_format( $amount, $currency = '' ) {
		if ( empty( $currency ) ) {
			$currency = self::get_currency();
		}

		$amount = floatval( $amount );
		return self::is_zero_decimal_currency( $currency )
			? $amount
			: $amount / 100;
	}

	/**
	 * Get supported payment gateways
	 *
	 * @return array<string, array<string, mixed>> Gateway configurations.
	 * @since 0.0.1
	 */
	public static function get_supported_gateways() {
		return apply_filters(
			'suredonation_payment_gateways',
			[
				'stripe' => [
					'label'       => __( 'Stripe', 'suredonation' ),
					'description' => __( 'Accept payments via Stripe', 'suredonation' ),
					'enabled'     => true,
				],
			]
		);
	}

	/**
	 * Check if a gateway is enabled
	 *
	 * @param string $gateway Gateway name.
	 * @return bool True if enabled.
	 * @since 0.0.1
	 */
	public static function is_gateway_enabled( $gateway ) {
		$gateways = self::get_supported_gateways();
		return ! empty( $gateways[ $gateway ]['enabled'] );
	}

	/**
	 * Validate payment amount against stored form configuration.
	 *
	 * This function verifies that the payment amount submitted matches the
	 * configured values in the form's payment block settings stored in post meta.
	 * It handles both fixed and variable (minimum) amount validations.
	 *
	 * This is the PRIMARY security function that prevents payment amount manipulation.
	 * It validates against IMMUTABLE configuration stored when the form was saved,
	 * not against request data which can be manipulated.
	 *
	 * @since 0.0.1
	 * @param float  $amount   Amount in major currency units (e.g., dollars, not cents).
	 * @param string $currency Currency code (e.g., 'USD', 'EUR').
	 * @param int    $form_id  WordPress post ID of the donation form.
	 * @param string $block_id Block identifier for the payment block.
	 * @return array<mixed> Validation result.
	 */
	public static function validate_payment_amount( $amount, $currency, $form_id, $block_id ) {
		// Retrieve block configuration from post meta.
		$block_config = \SureDonation\Inc\Field_Validation::get_or_migrate_block_config_for_legacy_form( $form_id );

		// Check if block config exists.
		if ( empty( $block_config ) || ! is_array( $block_config ) ) {
			return [
				'valid'   => false,
				'message' => __( 'Invalid form configuration.', 'suredonation' ),
			];
		}

		// Check if payment block exists in configuration.
		if ( ! isset( $block_config[ $block_id ] ) || ! is_array( $block_config[ $block_id ] ) ) {
			return [
				'valid'   => false,
				'message' => __( 'Payment configuration not found for this form.', 'suredonation' ),
			];
		}

		$payment_config = $block_config[ $block_id ];

		// Validate currency matches global setting.
		$global_currency    = strtolower( self::get_currency() );
		$submitted_currency = strtolower( $currency );
		if ( $global_currency !== $submitted_currency ) {
			return [
				'valid'   => false,
				/* translators: 1: expected currency, 2: received currency */
				'message' => sprintf( __( 'Currency mismatch: expected %1$s, received %2$s.', 'suredonation' ), strtoupper( $global_currency ), strtoupper( $submitted_currency ) ),
			];
		}

		// Get amount type (fixed or variable).
		// Default to 'fixed' if not set - this is the safest default for security.
		$amount_type = $payment_config['amount_type'] ?? 'fixed';

		// Validate based on amount type.
		if ( 'fixed' === $amount_type ) {
			// Fixed amount validation - must match exactly.
			$configured_amount = isset( $payment_config['fixed_amount'] ) ? floatval( Helper::get_string_value( $payment_config['fixed_amount'] ) ) : 10.00;

			// Allow small floating point difference (0.01) due to rounding.
			if ( abs( $amount - $configured_amount ) > 0.01 ) {
				return [
					'valid'   => false,
					/* translators: %s: expected amount with currency */
					'message' => sprintf( __( 'Payment amount must be exactly %s.', 'suredonation' ), self::format_amount( $configured_amount, $currency ) ),
				];
			}
		} elseif ( 'variable' === $amount_type ) {
			// Variable amount validation - must be >= minimum.
			$minimum_amount = isset( $payment_config['minimum_amount'] ) ? floatval( Helper::get_string_value( $payment_config['minimum_amount'] ) ) : 1.0;

			if ( $amount < $minimum_amount ) {
				return [
					'valid'   => false,
					/* translators: %s: minimum amount with currency */
					'message' => sprintf( __( 'Payment amount must be at least %s.', 'suredonation' ), self::format_amount( $minimum_amount, $currency ) ),
				];
			}

			// Additional validation for multi-choice amount fields.
			$dynamic_validation = self::validate_dynamic_amount_field( $payment_config, $block_config, $amount, $currency );
			if ( null !== $dynamic_validation ) {
				return $dynamic_validation;
			}
		}

		// Stripe minimum is approximately $0.50 USD (varies by currency).
		$stripe_minimum = 0.50;
		if ( $amount < $stripe_minimum ) {
			return [
				'valid'   => false,
				/* translators: %s: minimum amount */
				'message' => sprintf( __( 'Payment amount must be at least %s.', 'suredonation' ), self::format_amount( $stripe_minimum, $currency ) ),
			];
		}

		// Validation passed.
		return [
			'valid'   => true,
			'message' => '',
		];
	}

	/**
	 * Store payment intent metadata for verification.
	 *
	 * This stores the expected payment amount when creating a payment intent,
	 * allowing the webhook to verify the actual charged amount matches.
	 *
	 * @param string               $payment_intent_id The Stripe payment intent ID.
	 * @param array<string, mixed> $metadata          The metadata to store (amount, currency, campaign_id, donation_id).
	 * @return bool True on success.
	 * @since 0.0.1
	 */
	public static function store_payment_intent_metadata( $payment_intent_id, $metadata ) {
		$transient_key = 'suredonation_pi_' . $payment_intent_id;
		// Store for 24 hours (webhook should arrive within minutes).
		return set_transient( $transient_key, $metadata, DAY_IN_SECONDS );
	}

	/**
	 * Get stored payment intent metadata.
	 *
	 * @param string $payment_intent_id The Stripe payment intent ID.
	 * @return array<string, mixed>|false The stored metadata or false if not found.
	 * @since 0.0.1
	 */
	public static function get_payment_intent_metadata( $payment_intent_id ) {
		$transient_key = 'suredonation_pi_' . $payment_intent_id;
		$metadata      = get_transient( $transient_key );
		if ( is_array( $metadata ) ) {
			return $metadata;
		}
		return false;
	}

	/**
	 * Delete stored payment intent metadata after verification.
	 *
	 * @param string $payment_intent_id The Stripe payment intent ID.
	 * @return bool True on success.
	 * @since 0.0.1
	 */
	public static function delete_payment_intent_metadata( $payment_intent_id ) {
		$transient_key = 'suredonation_pi_' . $payment_intent_id;
		return delete_transient( $transient_key );
	}

	/**
	 * Verify payment intent amount matches expected amount.
	 *
	 * This is called by the webhook handler to detect amount manipulation.
	 *
	 * @param string $payment_intent_id The Stripe payment intent ID.
	 * @param int    $actual_amount     The actual amount charged (in cents).
	 * @param string $currency          The currency code.
	 * @return bool|WP_Error True if amounts match, WP_Error if mismatch or not found.
	 * @since 0.0.1
	 */
	public static function verify_payment_intent_amount( $payment_intent_id, $actual_amount, $currency ) {
		$metadata = self::get_payment_intent_metadata( $payment_intent_id );

		if ( false === $metadata ) {
			// No stored metadata - could be an old payment or external payment.
			// Log warning but don't fail (for backwards compatibility).
			return true;
		}

		$amount_value      = $metadata['amount_cents'] ?? 0;
		$expected_amount   = is_numeric( $amount_value ) ? (int) $amount_value : 0;
		$currency_value    = $metadata['currency'] ?? '';
		$expected_currency = is_string( $currency_value ) ? strtolower( $currency_value ) : '';

		// Verify currency matches.
		if ( strtolower( $currency ) !== $expected_currency ) {
			return new \WP_Error(
				'currency_mismatch',
				sprintf(
					/* translators: 1: expected currency, 2: actual currency */
					__( 'Currency mismatch. Expected %1$s but received %2$s.', 'suredonation' ),
					strtoupper( $expected_currency ),
					strtoupper( $currency )
				)
			);
		}

		// Verify amount matches (allow 1 cent tolerance for rounding).
		if ( abs( $actual_amount - $expected_amount ) > 1 ) {
			return new \WP_Error(
				'amount_mismatch',
				sprintf(
					/* translators: 1: expected amount, 2: actual amount */
					__( 'Amount mismatch detected. Expected %1$s but received %2$s. Possible payment manipulation.', 'suredonation' ),
					self::format_amount( self::amount_from_stripe_format( $expected_amount, $currency ), $currency ),
					self::format_amount( self::amount_from_stripe_format( $actual_amount, $currency ), $currency )
				)
			);
		}

		// Cleanup after successful verification.
		self::delete_payment_intent_metadata( $payment_intent_id );

		return true;
	}

	/**
	 * Validate dynamic amount field from multi-choice or number block.
	 *
	 * @param array<string, mixed> $payment_config Payment block configuration.
	 * @param array<string, mixed> $block_config   All block configurations.
	 * @param float                $amount         Submitted amount.
	 * @param string               $currency       Currency code.
	 * @return array<mixed>|null Validation result array or null if validation passes.
	 * @since 0.0.1
	 */
	private static function validate_dynamic_amount_field( $payment_config, $block_config, $amount, $currency ) {
		// Check if variable amount field block name is set.
		$dynamic_amount_field_block_name = isset( $payment_config['variable_amount_field_block_name'] ) && is_string( $payment_config['variable_amount_field_block_name'] ) ? $payment_config['variable_amount_field_block_name'] : '';

		if ( empty( $dynamic_amount_field_block_name ) ) {
			// Return null because it can be old form configuration.
			return null;
		}

		// Get the slug of the variable amount field.
		$variable_amount_field_slug = ! empty( $payment_config['variable_amount_field'] ) && is_string( $payment_config['variable_amount_field'] ) ? $payment_config['variable_amount_field'] : '';

		// Find the block config for the variable amount field by matching slug and block name.
		$variable_amount_block_config = self::get_block_config_by_name_and_slug( $block_config, $dynamic_amount_field_block_name, $variable_amount_field_slug );

		// Handle number block validation.
		if ( 'sd/number' === $dynamic_amount_field_block_name ) {
			return self::validate_number_field_amount( $variable_amount_block_config, $amount, $currency );
		}

		// Handle multi-choice block validation.
		if ( 'sd/multi-choice' === $dynamic_amount_field_block_name ) {
			return self::validate_multi_choice_amount( $variable_amount_block_config, $amount, $currency );
		}

		// Unknown block type - allow but log for debugging.
		return null;
	}

	/**
	 * Validate amount from number field against configured min/max.
	 *
	 * @param array<string, mixed>|null $number_block_config Number block configuration.
	 * @param float                     $amount              Submitted amount.
	 * @param string                    $currency            Currency code.
	 * @return array<string, mixed>|null Validation result array or null if validation passes.
	 * @since 0.0.1
	 */
	private static function validate_number_field_amount( $number_block_config, $amount, $currency ) {
		// If no config found, allow the amount (old forms).
		if ( empty( $number_block_config ) || ! is_array( $number_block_config ) ) {
			return null;
		}

		// Validate min value if configured.
		if ( isset( $number_block_config['min'] ) && is_numeric( $number_block_config['min'] ) ) {
			$min_value = (float) $number_block_config['min'];
			if ( $amount < $min_value - 0.01 ) { // Allow small tolerance.
				return [
					'valid'   => false,
					/* translators: %s: minimum amount with currency */
					'message' => sprintf( __( 'Payment amount must be at least %s.', 'suredonation' ), self::format_amount( $min_value, $currency ) ),
				];
			}
		}

		// Validate max value if configured.
		if ( isset( $number_block_config['max'] ) && is_numeric( $number_block_config['max'] ) ) {
			$max_value = (float) $number_block_config['max'];
			if ( $amount > $max_value + 0.01 ) { // Allow small tolerance.
				return [
					'valid'   => false,
					/* translators: %s: maximum amount with currency */
					'message' => sprintf( __( 'Payment amount cannot exceed %s.', 'suredonation' ), self::format_amount( $max_value, $currency ) ),
				];
			}
		}

		return null;
	}

	/**
	 * Validate amount from multi-choice field against configured options.
	 *
	 * @param array<string, mixed>|null $multi_choice_config Multi-choice block configuration.
	 * @param float                     $amount              Submitted amount.
	 * @param string                    $currency            Currency code.
	 * @return array<string, mixed>|null Validation result array or null if validation passes.
	 * @since 0.0.1
	 */
	private static function validate_multi_choice_amount( $multi_choice_config, $amount, $currency ) {
		unset( $currency ); // Unused.
		// Verify the variable amount block config was found.
		if ( empty( $multi_choice_config ) || ! is_array( $multi_choice_config ) ) {
			return [
				'valid'   => false,
				'message' => __( 'Variable amount field configuration not found.', 'suredonation' ),
			];
		}

		// Check if single selection is enabled (radio buttons).
		$choice_type         = $multi_choice_config['choice_type'] ?? 'radio';
		$is_single_selection = 'radio' === $choice_type;

		// Only validate amount matches options if single selection is enabled.
		if ( $is_single_selection ) {
			// Validate that submitted amount matches one of the allowed option values.
			$allowed_options = $multi_choice_config['options'] ?? [];
			if ( empty( $allowed_options ) || ! is_array( $allowed_options ) ) {
				return [
					'valid'   => false,
					'message' => __( 'No payment options are configured for this field.', 'suredonation' ),
				];
			}

			// Extract allowed values from options.
			$allowed_values = [];
			foreach ( $allowed_options as $option ) {
				if ( isset( $option['value'] ) && ! empty( $option['value'] ) ) {
					$allowed_values[] = floatval( $option['value'] );
				}
			}

			// Check if submitted amount matches any allowed value.
			$amount_is_valid = false;
			foreach ( $allowed_values as $allowed_value ) {
				// Allow small floating point difference (0.01) due to rounding.
				if ( abs( $amount - $allowed_value ) <= 0.01 ) {
					$amount_is_valid = true;
					break;
				}
			}

			if ( ! $amount_is_valid ) {
				return [
					'valid'   => false,
					'message' => __( 'Invalid payment amount. Please select a valid amount from the available options.', 'suredonation' ),
				];
			}
		}

		// Validation passed for multi-choice field.
		return null;
	}

	/**
	 * Get block configuration by block name and slug.
	 *
	 * @param array<mixed> $block_config All block configurations.
	 * @param string       $block_name   Block name to search for.
	 * @param string       $slug         Slug to match.
	 * @return array<string, mixed>|null Block configuration if found, null otherwise.
	 * @since 0.0.1
	 */
	private static function get_block_config_by_name_and_slug( $block_config, $block_name, $slug ) {
		foreach ( $block_config as $config ) {
			if ( empty( $config ) || ! is_array( $config ) ) {
				continue;
			}

			if ( isset( $config['slug'] ) && $config['slug'] === $slug && isset( $config['block_name'] ) && $config['block_name'] === $block_name ) {
				return $config;
			}
		}
		return null;
	}
}
