<?php
/**
 * Condition: Order discount IDs comparison.
 *
 * Evaluates whether the order's applied discount IDs include (or exclude)
 * specific discounts.
 *
 * Uses set-membership semantics:
 *   = (contains)         : Returns true if the order has ANY of the specified discount codes.
 *   != (does not contain) : Returns true if the order has NONE of the specified discount codes.
 *
 * Ordering operators (>, >=, <, <=) are not applicable and return false.
 *
 * @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\ParseData;
use EDD\Pro\Emails\ConditionalTags\Utilities\Tags;

/**
 * Class OrderDiscount
 *
 * @since 3.6.6
 */
class OrderDiscount extends AbstractCondition {
	use ParseData;

	/**
	 * Supported operators.
	 *
	 * @since 3.6.6
	 * @var string[]
	 */
	protected $supported_operators = array( '=', '!=' );

	/**
	 * Supported contexts.
	 *
	 * @since 3.6.6
	 * @var string[]
	 */
	protected $supported_contexts = array( 'order' );

	/**
	 * Get the key for this condition.
	 *
	 * @since 3.6.6
	 *
	 * @return string Condition key.
	 */
	public function get_key() {
		return 'discount_codes';
	}

	/**
	 * Get the label for this condition.
	 *
	 * @since 3.6.6
	 *
	 * @return string Condition label.
	 */
	public function get_label() {
		return __( 'Order Discount Code', 'easy-digital-downloads' );
	}

	/**
	 * Get custom operator labels for the UI.
	 *
	 * @since 3.6.6
	 *
	 * @return array<string, string>
	 */
	public function get_operator_labels() {
		return array(
			'='  => __( 'contains', 'easy-digital-downloads' ),
			'!=' => __( 'does not contain', 'easy-digital-downloads' ),
		);
	}

	/**
	 * Get the UI input type descriptor for the value field.
	 *
	 * Returns a searchable multi-select seeded with existing discount codes.
	 * The 'search' key enables Chosen AJAX search via the `edd_discount_search` endpoint.
	 *
	 * @since 3.6.6
	 *
	 * @return array UI input type descriptor.
	 */
	public function get_ui_input_type() {
		$discounts = edd_get_discounts(
			array(
				'number'  => 30,
				'orderby' => 'code',
				'order'   => 'ASC',
			)
		);

		// If no Discounts exist, return null to signal that this condition should not be shown in the UI.
		if ( empty( $discounts ) || is_wp_error( $discounts ) ) {
			return null; // do not show in UI.
		}

		$options = array();
		foreach ( $discounts as $discount ) {
			$discount_id = isset( $discount->id ) ? absint( $discount->id ) : 0;
			$code        = isset( $discount->code ) ? (string) $discount->code : '';
			if ( 0 === $discount_id || '' === $code ) {
				continue;
			}

			$label = $code;
			if ( ! empty( $discount->name ) && $discount->name !== $code ) {
				$label = $code . ' - ' . $discount->name;
			}

			$options[] = array(
				'value' => (string) $discount_id,
				'label' => $label,
			);
		}

		if ( empty( $options ) ) {
			return null; // do not show in UI.
		}

		return array(
			'type'     => 'select',
			'options'  => $options,
			'search'   => 'discount',
			'multiple' => true,
		);
	}

	/**
	 * Evaluate if the order's discount IDs match (or don't match) the given value.
	 *
	 * = returns true if at least one provided ID matches an applied discount ID.
	 * != returns true only if none of the provided IDs match.
	 *
	 * @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 discount ID(s) to check for (comma-separated).
	 * @param string $operator   The comparison operator (= or !=).
	 * @param mixed  $context    Additional contextual data for evaluation.
	 * @return bool
	 */
	public function evaluate( $object_id, $edd_object, $email, $value, $operator = '=', $context = '' ) {
		if ( ! $this->is_context_allowed( $context ) ) {
			return false;
		}

		if ( ! in_array( $operator, $this->supported_operators, true ) ) {
			return false;
		}

		$expected_ids = $this->parse_ids( $value );
		if ( empty( $expected_ids ) ) {
			$expected_ids = $this->get_discount_ids_from_codes( $value );
		}
		if ( empty( $expected_ids ) ) {
			return '!=' === $operator;
		}

		$actual_ids = $this->get_discount_ids( $edd_object, $object_id, $context );
		if ( empty( $actual_ids ) ) {
			return '!=' === $operator;
		}

		$has_match = ! empty( array_intersect( $expected_ids, $actual_ids ) );

		return '!=' === $operator ? ! $has_match : $has_match;
	}

	/**
	 * Retrieve discount IDs from the Order object or email tag callback.
	 *
	 * Attempts to read IDs directly from the Order's discount adjustments.
	 * If no Order is available, falls back to resolving IDs via the
	 * {discount_codes} tag output.
	 *
	 * @since 3.6.6
	 *
	 * @param object $edd_object The EDD object.
	 * @param int    $object_id  The object ID.
	 * @param mixed  $context    The email context.
	 * @return int[] Array of discount IDs.
	 */
	private function get_discount_ids( $edd_object, $object_id, $context ) {
		// Prefer IDs from order adjustments when possible.
		if ( $edd_object instanceof \EDD\Orders\Order ) {
			$adjustments = $edd_object->get_discounts();
			if ( ! empty( $adjustments ) ) {
				$ids = array_map( 'absint', wp_list_pluck( $adjustments, 'type_id' ) );
				$ids = array_filter( $ids );

				return array_values( array_unique( $ids ) );
			}
		}

		// Fallback: resolve IDs from the {discount_codes} tag output.
		$tag = Tags::get_tag_by_name( $this->get_key(), $context );
		if ( $tag && isset( $tag['func'] ) && is_callable( $tag['func'] ) ) {
			try {
				$codes = Tags::do_tag( $tag, $object_id, $edd_object, $context );
				$ids   = $this->get_discount_ids_from_codes( $codes );
				if ( ! empty( $ids ) ) {
					return $ids;
				}
			} catch ( \Throwable $e ) {
				// Continue to fallback.
			}
		}

		return array();
	}

	/**
	 * Resolve discount IDs from raw code strings.
	 *
	 * @since 3.6.6
	 *
	 * @param mixed $raw_codes Raw discount code input.
	 * @return int[]
	 */
	private function get_discount_ids_from_codes( $raw_codes ) {
		$codes = $this->parse_string_list( $raw_codes );
		if ( empty( $codes ) ) {
			return array();
		}

		$ids = array();
		foreach ( $codes as $code ) {
			$discount_id = edd_get_discount_id_by_code( $code );
			if ( ! empty( $discount_id ) ) {
				$ids[] = absint( $discount_id );
			}
		}

		$ids = array_filter( $ids );

		return array_values( array_unique( $ids ) );
	}
}
