<?php

namespace WPDRMS\ASP\Priorities;

use WPDRMS\ASP\Query\SearchQueryArgs;
use WPDRMS\Utils\MB;

if ( ! defined( 'ABSPATH' ) ) {
	die( '-1' );
}

/**
 * Evaluates a priority group's "Applies when" GATE against a search request.
 *
 * This is the per-search boolean that decides whether a group participates at all — distinct
 * from the per-post boost rules (tax/cf/post) that decide which posts get the boost. It reads
 * the resolved {@see SearchQueryArgs::$post_tax_filter} / {@see SearchQueryArgs::$post_meta_filter}
 * arrays, i.e. the filters that are actually active on this search, not the raw submitted form.
 *
 * Combination model: multiple condition rows of the same type are OR-ed together; the per-type
 * results are then combined with the group's `condition_logic` (AND / OR across types).
 *
 * Groups stored before this feature have no `conditions` key and fall back to the legacy
 * single-instance + single-phrase gate.
 */
final class PriorityGateEvaluator {

	/**
	 * @param array<string, mixed> $group The priority group (array form, as stored).
	 * @param SearchQueryArgs      $args  The resolved search arguments.
	 * @param string               $s     The search phrase.
	 */
	public static function matches( array $group, SearchQueryArgs $args, string $s ): bool {
		$conditions = isset( $group['conditions'] ) && is_array( $group['conditions'] )
			? $group['conditions'] : array();

		if ( empty( $conditions ) ) {
			return self::matchesLegacy( $group, $args, $s );
		}

		// OR within a type: a type matches if any of its rows match.
		$by_type = array();
		foreach ( $conditions as $condition ) {
			if ( ! is_array( $condition ) ) {
				continue;
			}
			$type = (string) ( $condition['type'] ?? '' );
			if ( $type === '' ) {
				continue;
			}
			$by_type[ $type ] = ( $by_type[ $type ] ?? false ) || self::matchesCondition( $type, $condition, $args, $s );
		}

		// A conditions array that contained nothing usable imposes no constraint.
		if ( empty( $by_type ) ) {
			return true;
		}

		if ( ( $group['condition_logic'] ?? 'and' ) === 'or' ) {
			return in_array( true, $by_type, true );
		}
		// AND across types.
		foreach ( $by_type as $matched ) {
			if ( ! $matched ) {
				return false;
			}
		}
		return true;
	}

	/**
	 * @param string               $type      The condition type.
	 * @param array<string, mixed> $condition The condition row.
	 * @param SearchQueryArgs      $args      The resolved search arguments.
	 * @param string               $s         The search phrase.
	 */
	private static function matchesCondition( string $type, array $condition, SearchQueryArgs $args, string $s ): bool {
		switch ( $type ) {
			case 'instance':
				$ids = array_values( array_filter( array_map( 'intval', (array) ( $condition['instance_ids'] ?? array() ) ) ) );
				return empty( $ids ) || in_array( (int) $args['_id'], $ids, true );
			case 'phrase':
				return self::matchesPhrase( (string) ( $condition['phrase'] ?? '' ), (string) ( $condition['phrase_logic'] ?? '' ), $s );
			case 'taxonomy':
				return self::matchesTaxonomy( $condition, $args );
			case 'meta':
				return self::matchesMeta( $condition, $args );
		}
		return false;
	}

	private static function matchesPhrase( string $phrase, string $logic, string $s ): bool {
		// An empty phrase (or 'disabled' logic) imposes no constraint.
		if ( $phrase === '' ) {
			return true;
		}
		switch ( $logic ) {
			case 'any':
				return MB::strpos( $s, $phrase ) !== false;
			case 'exact':
				return $s === $phrase;
			case 'start':
				return MB::strpos( $s, $phrase ) === 0;
			case 'end':
				return MB::substr( $s, -MB::strlen( $phrase ) ) === $phrase;
		}
		return true;
	}

	/**
	 * Matches when the active taxonomy filter for the configured taxonomy includes (operator IN)
	 * or excludes (operator NOT IN) any of the configured term ids.
	 *
	 * @param array<string, mixed> $condition The taxonomy condition row.
	 * @param SearchQueryArgs      $args      The resolved search arguments.
	 */
	private static function matchesTaxonomy( array $condition, SearchQueryArgs $args ): bool {
		$taxonomy = (string) ( $condition['taxonomy'] ?? '' );
		$term_ids = array_values( array_filter( array_map( 'intval', (array) ( $condition['term_ids'] ?? array() ) ) ) );
		if ( $taxonomy === '' || empty( $term_ids ) ) {
			return false;
		}
		$exclude = strtoupper( (string) ( $condition['operator'] ?? 'IN' ) ) === 'NOT IN';

		foreach ( $args['post_tax_filter'] as $filter ) {
			if ( ! is_array( $filter ) || (string) ( $filter['taxonomy'] ?? '' ) !== $taxonomy ) {
				continue;
			}
			$selected = $exclude
				? array_map( 'intval', (array) ( $filter['exclude'] ?? array() ) )
				: array_map( 'intval', (array) ( $filter['include'] ?? array() ) );
			if ( ! empty( array_intersect( $term_ids, $selected ) ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * Matches against the active custom-field filter on the configured meta key. IN / "=" match
	 * when the active selection intersects the configured value(s); NOT IN matches when the filter
	 * is present but does not; the numeric comparisons (>, >=, <, <=) match when any active value
	 * compares true against the configured value.
	 *
	 * @param array<string, mixed> $condition The meta condition row.
	 * @param SearchQueryArgs      $args      The resolved search arguments.
	 */
	private static function matchesMeta( array $condition, SearchQueryArgs $args ): bool {
		$key = (string) ( $condition['meta_key'] ?? '' );
		if ( $key === '' ) {
			return false;
		}
		$operator    = strtoupper( (string) ( $condition['operator'] ?? 'IN' ) );
		$cond_values = self::normaliseValues( $condition['value'] ?? array() );
		if ( empty( $cond_values ) ) {
			return false;
		}

		foreach ( $args['post_meta_filter'] as $filter ) {
			if ( ! is_array( $filter ) || (string) ( $filter['key'] ?? '' ) !== $key ) {
				continue;
			}
			$active = self::normaliseValues( $filter['value'] ?? array() );
			if ( in_array( $operator, array( '>', '>=', '<', '<=' ), true ) ) {
				if ( self::compareNumeric( $active, $operator, (float) $cond_values[0] ) ) {
					return true;
				}
				continue;
			}
			$intersect = array_intersect( $cond_values, $active );
			if ( $operator === 'NOT IN' ) {
				if ( ! empty( $active ) && empty( $intersect ) ) {
					return true;
				}
			} elseif ( ! empty( $intersect ) ) {
				return true;
			}
		}
		return false;
	}

	/**
	 * True when any of the active filter values numerically satisfies `value <op> threshold`.
	 *
	 * @param string[] $active   The active filter values.
	 * @param string   $operator One of >, >=, <, <=.
	 * @param float    $threshold The configured condition value.
	 */
	private static function compareNumeric( array $active, string $operator, float $threshold ): bool {
		foreach ( $active as $value ) {
			$value = (float) $value;
			switch ( $operator ) {
				case '>':
					if ( $value > $threshold ) {
						return true;
					}
					break;
				case '>=':
					if ( $value >= $threshold ) {
						return true;
					}
					break;
				case '<':
					if ( $value < $threshold ) {
						return true;
					}
					break;
				case '<=':
					if ( $value <= $threshold ) {
						return true;
					}
					break;
			}
		}
		return false;
	}

	/**
	 * @param mixed $value
	 * @return string[]
	 */
	private static function normaliseValues( $value ): array {
		if ( is_array( $value ) ) {
			return array_values( array_map( 'strval', $value ) );
		}
		if ( $value === '' || $value === null ) {
			return array();
		}
		return array( (string) $value );
	}

	/**
	 * Legacy gate: single instance + single phrase (the behaviour before flexible conditions).
	 *
	 * @param array<string, mixed> $group The priority group (array form, as stored).
	 * @param SearchQueryArgs      $args  The resolved search arguments.
	 * @param string               $s     The search phrase.
	 */
	private static function matchesLegacy( array $group, SearchQueryArgs $args, string $s ): bool {
		$instance = (int) ( $group['instance'] ?? 0 );
		if ( $instance !== 0 && $instance !== (int) $args['_id'] ) {
			return false;
		}
		$phrase = (string) ( $group['phrase'] ?? '' );
		if ( MB::strlen( $phrase ) > 0 ) {
			return self::matchesPhrase( $phrase, (string) ( $group['phrase_logic'] ?? '' ), $s );
		}
		return true;
	}
}
