<?php
/**
 * Condition: Proxy for any registered EDD email tag.
 *
 * This special condition class enables any EDD email tag to be used as
 * a condition in {if} blocks. When the ConditionalTags loader registers
 * tag proxies, it creates a TagProxy instance for each registered email
 * tag.
 *
 * The proxy invokes the email tag's callback to get the actual value,
 * then compares it against the expected value using the Comparison utility.
 *
 * @package     EDD\Pro\Emails\ConditionalTags\Conditions
 * @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\Conditions;

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

use EDD\Pro\Emails\ConditionalTags\Conditions\Traits\ContextAware;
use EDD\Pro\Emails\ConditionalTags\Utilities\Comparison;
use EDD\Pro\Emails\ConditionalTags\Utilities\Tags;

/**
 * Class TagProxy
 *
 * @since 3.6.6
 */
class TagProxy implements ConditionInterface {

	use ContextAware;

	/**
	 * Core comparable tag definitions keyed by tag key.
	 *
	 * Tags not listed here are unsupported for conditional comparisons by default.
	 *
	 * @since 3.6.6
	 * @var array
	 */
	const COMPARABLE_TAG_TYPES = array(
		// Currency tags.
		'subtotal'       => Comparison::TYPE_CURRENCY,
		'tax'            => Comparison::TYPE_CURRENCY,
		'fees_total'     => Comparison::TYPE_CURRENCY,
		'price'          => Comparison::TYPE_CURRENCY,
		'refund_amount'  => Comparison::TYPE_CURRENCY,
		// Number tags.
		'quantity'       => Comparison::TYPE_NUMBER,
		// Text tags.
		'payment_method' => Comparison::TYPE_TEXT,
	);

	/**
	 * The key of the email tag to proxy.
	 *
	 * This matches the tag name as registered via edd_add_email_tag()
	 * (e.g. 'name', 'download_list', 'date', etc.).
	 *
	 * @since 3.6.6
	 * @var string
	 */
	protected $key;

	/**
	 * The contexts in which this tag is available.
	 *
	 * Limits evaluation to specific email contexts (e.g. 'order', 'customer').
	 * Null means the tag is available in all contexts.
	 *
	 * @since 3.6.6
	 * @var array|null
	 */
	protected $contexts;

	/**
	 * Constructor.
	 *
	 * @since 3.6.6
	 *
	 * @param string     $key      The key of the email tag to proxy.
	 * @param array|null $contexts The contexts in which the tag is available.
	 */
	public function __construct( $key, $contexts = null ) {
		$this->key      = $key;
		$this->contexts = $contexts;
	}

	/**
	 * Get the key of the proxied email tag.
	 *
	 * @since 3.6.6
	 *
	 * @return string The tag key.
	 */
	public function get_key() {
		return $this->key;
	}

	/**
	 * Get the label for this condition.
	 *
	 * @since 3.6.6
	 *
	 * @return string Condition label.
	 */
	public function get_label() {
		$context = '';
		if ( ! empty( $this->contexts ) && is_array( $this->contexts ) ) {
			$context = (string) reset( $this->contexts );
		}

		$tag = Tags::get_tag_by_name( $this->get_key(), $context );
		if ( is_array( $tag ) ) {
			if ( ! empty( $tag['description'] ) ) {
				return (string) $tag['description'];
			}

			if ( ! empty( $tag['tag'] ) ) {
				return self::format_label_from_key( (string) $tag['tag'] );
			}
		}

		return self::format_label_from_key( $this->get_key() );
	}

	/**
	 * Get supported operators for this condition.
	 *
	 * @since 3.6.6
	 *
	 * @return string[] Supported operators.
	 */
	public function get_supported_operators() {
		$comparison_definition = self::get_tag_comparison_definition( $this->get_key() );
		if ( empty( $comparison_definition['comparable'] ) ) {
			return array();
		}

		$value_type = isset( $comparison_definition['type'] ) ? (string) $comparison_definition['type'] : Comparison::TYPE_TEXT;
		if ( in_array( $value_type, array( Comparison::TYPE_CURRENCY, Comparison::TYPE_NUMBER ), true ) ) {
			return Comparison::OPERATORS;
		}

		return array( '=', '!=' );
	}

	/**
	 * Get the email contexts supported by this condition.
	 *
	 * @since 3.6.6
	 *
	 * @return string[] Supported contexts.
	 */
	public function get_supported_contexts() {
		return empty( $this->contexts ) ? array() : array_values( (array) $this->contexts );
	}

	/**
	 * Get the UI input type descriptor for the value field.
	 *
	 * Derives the input type from the tag's comparison type so that
	 * currency and number tags render a number input, while text tags
	 * render a plain text input.
	 *
	 * @since 3.6.6
	 *
	 * @return array{type: string}
	 */
	public function get_ui_input_type() {
		$definition = self::get_tag_comparison_definition( $this->get_key() );
		if ( ! empty( $definition['comparable'] ) ) {
			$type = isset( $definition['type'] ) ? $definition['type'] : Comparison::TYPE_TEXT;
			if ( Comparison::TYPE_CURRENCY === $type ) {
				return array( 'type' => 'currency' );
			}
			if ( Comparison::TYPE_NUMBER === $type ) {
				return array( 'type' => 'number' );
			}
		}

		return array( 'type' => 'text' );
	}

	/**
	 * Evaluate the condition against the proxied email tag output.
	 *
	 * Resolves whether the tag is supported for conditional comparison first.
	 * If supported, invokes the tag callback to get the actual value and compares
	 * it against the expected value using the Comparison utility.
	 *
	 * @since 3.6.6
	 *
	 * @param int    $object_id  The EDD object ID.
	 * @param object $edd_object The EDD object.
	 * @param object $email      The EDD email object.
	 * @param mixed  $value      The expected value to compare against.
	 * @param string $operator   The comparison operator (=, !=, >, >=, <, <=).
	 * @param mixed  $context    The email context.
	 * @return bool True if the condition is met, false otherwise.
	 */
	public function evaluate( $object_id, $edd_object, $email, $value, $operator = '=', $context = '' ) {
		if ( ! is_object( $edd_object ) ) {
			return false;
		}

		$value = trim( (string) $value );
		if ( '' === $value ) {
			return false;
		}

		// Verify the current email context matches this tag's allowed contexts.
		if ( ! $this->is_context_allowed( $context ) ) {
			return false;
		}

		$tag = Tags::get_tag_by_name( $this->get_key(), $context );
		if ( ! $tag || ! isset( $tag['func'] ) || ! is_callable( $tag['func'] ) ) {
			return false;
		}

		$comparison_definition = self::get_tag_comparison_definition( $this->get_key() );
		if ( empty( $comparison_definition['comparable'] ) ) {
			edd_debug_log( sprintf( 'Conditional email tags: unsupported conditional tag "%s" was ignored.', $this->get_key() ) );
			return false;
		}

		try {
			$parameter = $email instanceof \EDD\Emails\Types\Email ? $email : $context;
			$actual    = Tags::do_tag( $tag, $object_id, $edd_object, $parameter );
			$actual    = trim( (string) $actual );

			if ( '' === $actual ) {
				return false;
			}

			return Comparison::compare( $actual, $value, $operator, $comparison_definition['type'] );
		} catch ( \Throwable $e ) {
			return false;
		}
	}

	/**
	 * Get the conditional comparison definition for a tag key.
	 *
	 * Tags not explicitly mapped are treated as unsupported.
	 *
	 * @since 3.6.6
	 *
	 * @param string $tag_key The email tag key.
	 * @return array{comparable: bool, type: string}
	 */
	public static function get_tag_comparison_definition( $tag_key ) {
		return self::get_core_comparison_definition( $tag_key );
	}

	/**
	 * Get all supported tags and their comparison value types.
	 *
	 * This method is useful for UI surfaces (for example, modal selectors)
	 * that need to display which tags are currently supported and how each
	 * tag is compared.
	 *
	 * @since 3.6.6
	 *
	 * @return array<string, string> Array of tag_key => comparison_type.
	 */
	public static function get_supported_tags() {
		return self::get_comparable_tag_types();
	}

	/**
	 * Get core comparison definition for a tag key.
	 *
	 * @since 3.6.6
	 *
	 * @param string $tag_key The email tag key.
	 * @return array{comparable: bool, type: string}
	 */
	private static function get_core_comparison_definition( $tag_key ) {
		$supported_tag_types = self::get_comparable_tag_types();
		if ( isset( $supported_tag_types[ $tag_key ] ) ) {
			return array(
				'comparable' => true,
				'type'       => $supported_tag_types[ $tag_key ],
			);
		}

		return self::definition_unsupported();
	}

	/**
	 * Get the map of supported tag keys to comparison types.
	 *
	 * @since 3.6.6
	 *
	 * @return array<string, string>
	 */
	private static function get_comparable_tag_types() {
		$tags = self::COMPARABLE_TAG_TYPES;

		/**
		 * Filters supported conditional tag keys and their value types.
		 *
		 * This is the extension point for registering or removing
		 * conditional-comparable email tags.
		 *
		 * Array format: array( 'tag_key' => 'text|currency|number', ... ).
		 *
		 * - Add a key to make a tag condition-comparable.
		 * - Remove a key (or return a map without it) to disable comparisons for that tag.
		 * - Invalid value types are normalized to `text`.
		 * - Invalid return values (non-array) fall back to core defaults.
		 *
		 * @since 3.6.6
		 *
		 * @param array<string, string> $tags Supported conditional tag map.
		 */
		$tags = apply_filters( 'edd_conditional_tags_supported_tags', $tags );
		if ( ! is_array( $tags ) ) {
			return self::COMPARABLE_TAG_TYPES;
		}

		$normalized_tags = array();
		foreach ( $tags as $tag_key => $value_type ) {
			$tag_key = trim( (string) $tag_key );
			if ( '' === $tag_key ) {
				continue;
			}

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

			$normalized_tags[ $tag_key ] = $value_type;
		}

		return $normalized_tags;
	}

	/**
	 * Build an unsupported comparison definition.
	 *
	 * @since 3.6.6
	 *
	 * @return array{comparable: bool, type: string}
	 */
	private static function definition_unsupported() {
		return array(
			'comparable' => false,
			'type'       => Comparison::TYPE_TEXT,
		);
	}

	/**
	 * Format a condition label from a tag key.
	 *
	 * @since 3.6.6
	 *
	 * @param string $key Tag key.
	 * @return string
	 */
	private static function format_label_from_key( $key ) {
		$key = str_replace( array( '-', '_' ), ' ', (string) $key );

		return ucwords( trim( $key ) );
	}
}
