<?php
/**
 * Shared comparison utility for conditional email tag evaluation.
 *
 * Provides a single, centralized method for comparing an actual value
 * against an expected value using a given operator. This eliminates
 * duplicated comparison logic across individual condition classes.
 *
 * Supported operators: =, !=, >, >=, <, <=
 *
 * @package     EDD\Pro\Emails\ConditionalTags\Utilities
 * @copyright   Copyright (c) 2026, Sandhills Development, LLC
 * @license     https://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since       3.6.6
 */

namespace EDD\Pro\Emails\ConditionalTags\Utilities;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore

/**
 * Class Comparison
 *
 * Centralizes value comparison for all condition evaluations.
 * Handles both numeric comparisons (with >, >=, <, <=) and
 * string comparisons (case-insensitive equality via = and !=).
 *
 * @since 3.6.6
 */
class Comparison {

	/**
	 * All supported comparison operators.
	 *
	 * @since 3.6.6
	 * @var array
	 */
	const OPERATORS = array( '=', '!=', '>', '>=', '<', '<=' );

	/**
	 * Text value type.
	 *
	 * @since 3.6.6
	 * @var string
	 */
	const TYPE_TEXT = 'text';

	/**
	 * Currency value type.
	 *
	 * @since 3.6.6
	 * @var string
	 */
	const TYPE_CURRENCY = 'currency';

	/**
	 * Number value type.
	 *
	 * @since 3.6.6
	 * @var string
	 */
	const TYPE_NUMBER = 'number';

	/**
	 * Get all supported comparison value types.
	 *
	 * @since 3.6.6
	 *
	 * @return string[]
	 */
	public static function get_supported_value_types() {
		return array(
			self::TYPE_TEXT,
			self::TYPE_CURRENCY,
			self::TYPE_NUMBER,
		);
	}

	/**
	 * Check if a given value type is supported.
	 *
	 * @since 3.6.6
	 *
	 * @param string $value_type The value type to validate.
	 * @return bool
	 */
	public static function is_valid_value_type( $value_type ) {
		return in_array( $value_type, self::get_supported_value_types(), true );
	}

	/**
	 * Check if a given operator string is valid.
	 *
	 * @since 3.6.6
	 *
	 * @param string $operator The operator to validate.
	 * @return bool True if the operator is supported.
	 */
	public static function is_valid_operator( $operator ) {
		return in_array( $operator, self::OPERATORS, true );
	}

	/**
	 * Compare an actual value against an expected value using the given operator.
	 *
	 * The value type determines how values are normalized before comparison:
	 * - text: case-insensitive string comparison.
	 * - currency: normalize via edd_format_amount() in typed mode.
	 * - number: cast both sides to float.
	 *
	 * @since 3.6.6
	 *
	 * @param mixed  $actual     The actual value from the order/object.
	 * @param mixed  $expected   The expected value from the condition expression.
	 * @param string $operator   The comparison operator. Defaults to '=' (equality).
	 * @param string $value_type The value type. Defaults to self::TYPE_TEXT.
	 * @return bool True if the comparison passes, false otherwise.
	 */
	public static function compare( $actual, $expected, $operator = '=', $value_type = self::TYPE_TEXT ): bool {
		$actual     = trim( (string) $actual );
		$expected   = trim( (string) $expected );
		$operator   = trim( (string) $operator );
		$value_type = (string) $value_type;

		if ( ! self::is_valid_operator( $operator ) ) {
			return false;
		}

		if ( ! self::is_valid_value_type( $value_type ) ) {
			$value_type = self::TYPE_TEXT;
		}

		if ( self::TYPE_CURRENCY === $value_type ) {
			$normalized_actual   = edd_sanitize_amount( $actual );
			$normalized_expected = edd_sanitize_amount( $expected );
			return self::compare_numeric( $normalized_actual, $normalized_expected, $operator );
		}

		if ( self::TYPE_NUMBER === $value_type ) {
			return self::compare_numeric( (float) $actual, (float) $expected, $operator );
		}

		return self::compare_string( $actual, $expected, $operator );
	}

	/**
	 * Perform a numeric (float) comparison.
	 *
	 * @since 3.6.6
	 *
	 * @param string $actual   The actual value.
	 * @param string $expected The expected value.
	 * @param string $operator The comparison operator.
	 * @return bool
	 */
	private static function compare_numeric( $actual, $expected, $operator ) {
		if ( ! is_numeric( $actual ) || ! is_numeric( $expected ) ) {
			return false;
		}

		$actual_float   = (float) $actual;
		$expected_float = (float) $expected;

		switch ( $operator ) {
			case '>':
				return $actual_float > $expected_float;
			case '>=':
				return $actual_float >= $expected_float;
			case '<':
				return $actual_float < $expected_float;
			case '<=':
				return $actual_float <= $expected_float;
			case '!=':
				return $actual_float !== $expected_float;
			case '=':
			default:
				return $actual_float === $expected_float;
		}
	}

	/**
	 * Perform a case-insensitive string comparison.
	 *
	 * Only supports = (equality) and != (inequality) operators.
	 * Other operators return false for non-numeric strings.
	 *
	 * @since 3.6.6
	 *
	 * @param string $actual   The actual value.
	 * @param string $expected The expected value.
	 * @param string $operator The comparison operator.
	 * @return bool
	 */
	private static function compare_string( $actual, $expected, $operator ) {
		$equals = strtolower( $actual ) === strtolower( $expected );

		switch ( $operator ) {
			case '!=':
				return ! $equals;
			case '=':
				return $equals;
			default:
				return false;
		}
	}
}
