<?php
/**
 * Registry for all available email conditions.
 *
 * @package     EDD\Pro\Emails\ConditionalTags
 * @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;

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

use EDD\Pro\Emails\ConditionalTags\Conditions\AbstractCondition;
use EDD\Pro\Emails\ConditionalTags\Conditions\ConditionInterface;
use EDD\Pro\Emails\ConditionalTags\Conditions\TagProxy;

/**
 * Class ConditionRegistry
 *
 * @since 3.6.6
 */
class ConditionRegistry {

	/**
	 * Registered conditions grouped by key.
	 *
	 * @since 3.6.6
	 * @var array<string, ConditionInterface[]>
	 */
	protected $conditions = array();

	/**
	 * Register a new condition.
	 *
	 * Multiple conditions may share the same key provided they serve
	 * different contexts. Duplicate context registrations are silently
	 * ignored so the first condition to register for a context wins.
	 *
	 * @since 3.6.6
	 *
	 * @param ConditionInterface $condition Condition object to register.
	 * @return void
	 */
	public function register( ConditionInterface $condition ) {
		$key = $condition->get_key();
		if ( ! isset( $this->conditions[ $key ] ) ) {
			$this->conditions[ $key ] = array();
		}

		$new_contexts = $condition->get_supported_contexts();
		if ( $this->has_conflicting_condition( $key, $new_contexts ) ) {
			return;
		}

		$this->conditions[ $key ][] = $condition;
	}

	/**
	 * Get a condition by key, optionally resolved by context.
	 *
	 * Resolution order when a context is provided:
	 *   1. Exact context match (condition whose contexts include the given context).
	 *   2. Universal fallback (condition with empty contexts array).
	 *
	 * When called without a context the first registered condition is returned
	 * for backward compatibility.
	 *
	 * @since 3.6.6
	 *
	 * @param string $key     Condition key.
	 * @param string $context Optional email context for resolution.
	 * @return ConditionInterface|null Condition object or null if not found.
	 */
	public function get( $key, $context = '' ) {
		if ( ! isset( $this->conditions[ $key ] ) ) {
			return null;
		}

		$candidates = $this->conditions[ $key ];

		if ( '' === $context ) {
			return reset( $candidates );
		}

		$universal = null;
		foreach ( $candidates as $condition ) {
			$supported = $condition->get_supported_contexts();
			if ( empty( $supported ) ) {
				$universal = $condition;
				continue;
			}
			if ( in_array( $context, $supported, true ) ) {
				return $condition;
			}
		}

		return $universal;
	}

	/**
	 * Check whether any condition is registered for a key.
	 *
	 * @since 3.6.6
	 *
	 * @param string $key     Condition key.
	 * @param string $context Optional email context to check.
	 * @return bool
	 */
	public function has( $key, $context = '' ) {
		if ( ! isset( $this->conditions[ $key ] ) || empty( $this->conditions[ $key ] ) ) {
			return false;
		}

		if ( '' === $context ) {
			return true;
		}

		foreach ( $this->conditions[ $key ] as $condition ) {
			$supported = $condition->get_supported_contexts();
			if ( empty( $supported ) || in_array( $context, $supported, true ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Get all registered conditions as a flat array.
	 *
	 * @since 3.6.6
	 *
	 * @return ConditionInterface[] Array of condition objects.
	 */
	public function all() {
		$flat = array();
		foreach ( $this->conditions as $conditions ) {
			foreach ( $conditions as $condition ) {
				$flat[] = $condition;
			}
		}

		return $flat;
	}

	/**
	 * Get all registered first-class conditions.
	 *
	 * Excludes TagProxy instances that mirror email tags.
	 *
	 * @since 3.6.6
	 *
	 * @return ConditionInterface[] Array of first-class condition objects.
	 */
	public function get_first_class_conditions() {
		return array_filter(
			$this->all(),
			function ( ConditionInterface $condition ) {
				return ! $condition instanceof TagProxy;
			}
		);
	}

	/**
	 * Get first-class condition metadata for UI consumers.
	 *
	 * When multiple conditions share the same key (e.g. core's Download for
	 * 'order' and recurring's Download for 'subscription'), their operators
	 * and contexts are merged into a single metadata entry so the UI sees one
	 * unified option. The seed condition — which provides the label and
	 * inputType — is resolved via get() so the context-appropriate condition
	 * wins.
	 *
	 * @since 3.6.6
	 *
	 * @param string $context Optional email context for seed resolution.
	 * @return array<string, array> First-class metadata keyed by condition key.
	 */
	public function get_first_class_condition_metadata( $context = '' ) {
		$metadata = array();

		foreach ( $this->conditions as $key => $conditions ) {
			// Resolve the seed condition for this context.
			// Falls back to first registered (or universal) via get().
			$seed = $this->get( $key, $context );
			if ( ! $seed || $seed instanceof TagProxy ) {
				continue;
			}

			$input_type = $seed->get_ui_input_type();
			if ( null === $input_type || ! isset( $input_type['type'] ) ) {
				continue;
			}

			$operator_labels = ( $seed instanceof AbstractCondition )
				? $seed->get_operator_labels()
				: array();

			$metadata[ $key ] = array(
				'key'            => $key,
				'label'          => $seed->get_label(),
				'operators'      => array(),
				'operatorLabels' => $operator_labels,
				'contexts'       => array(),
				'inputType'      => $input_type,
			);

			// Merge operators, contexts, and operatorLabels from ALL conditions
			// sharing this key so the UI has the full union.
			// Also upgrade inputType from 'text' to a richer type if available.
			foreach ( $conditions as $condition ) {
				if ( $condition instanceof TagProxy ) {
					continue;
				}

				if ( $condition instanceof AbstractCondition ) {
					$metadata[ $key ]['operatorLabels'] = array_merge(
						$metadata[ $key ]['operatorLabels'],
						$condition->get_operator_labels()
					);
				}

				$metadata[ $key ]['operators'] = array_values(
					array_unique(
						array_merge(
							$metadata[ $key ]['operators'],
							(array) $condition->get_supported_operators()
						)
					)
				);

				$metadata[ $key ]['contexts'] = array_values(
					array_unique(
						array_merge(
							$metadata[ $key ]['contexts'],
							(array) $condition->get_supported_contexts()
						)
					)
				);

				// Upgrade inputType from the default 'text' to a richer type
				// when another condition sharing the same key provides one.
				if ( 'text' === $metadata[ $key ]['inputType']['type'] ) {
					$candidate_type = $condition->get_ui_input_type();
					if ( isset( $candidate_type['type'] ) && 'text' !== $candidate_type['type'] ) {
						$metadata[ $key ]['inputType'] = $candidate_type;
					}
				}
			}
		}

		return $metadata;
	}

	/**
	 * Check whether an existing condition conflicts with the given contexts.
	 *
	 * A conflict exists when an already-registered condition for the same key
	 * covers the same context(s) as the incoming one, meaning the new condition
	 * would be a duplicate and should not be registered.
	 *
	 * @since 3.6.6
	 *
	 * @param string   $key          Condition key.
	 * @param string[] $new_contexts Contexts the incoming condition supports.
	 * @return bool True if a conflict is found and registration should be skipped.
	 */
	private function has_conflicting_condition( $key, array $new_contexts ) {
		if ( empty( $this->conditions[ $key ] ) ) {
			return false;
		}

		foreach ( $this->conditions[ $key ] as $existing ) {
			$existing_contexts = $existing->get_supported_contexts();

			// Two universal (empty-context) conditions for the same key conflict.
			if ( empty( $new_contexts ) && empty( $existing_contexts ) ) {
				return true;
			}

			// Two context-specific conditions with overlapping contexts conflict.
			if ( ! empty( $new_contexts ) && ! empty( $existing_contexts ) ) {
				if ( ! empty( array_intersect( $new_contexts, $existing_contexts ) ) ) {
					return true;
				}
			}
		}

		return false;
	}
}
