<?php
/**
 * Query builder for recommendations engine.
 *
 * @package PRECO
 */

namespace PRECO\Includes\Frontend\Engine\Filters;

use PRECO\Includes\Frontend\Engine\Sorting\SortingHelper;
use PRECO\Includes\Frontend\Engine\Filters\Query\DateRuleHelper;
use PRECO\Includes\Frontend\Engine\Filters\Query\ConstraintsFinalizer;
use PRECO\Includes\Frontend\Engine\Filters\Query\ContextResolver;
use PRECO\Includes\Frontend\Engine\Filters\Query\CurrentTaxRuleHelper;
use PRECO\Includes\Frontend\Engine\Filters\Query\GateRuleHelper;
use PRECO\Includes\Frontend\Engine\Filters\Query\IncludeExcludeRuleHelper;
use PRECO\Includes\Frontend\Engine\Filters\Query\MetaRuleHelper;
use PRECO\Includes\Frontend\Engine\Filters\Query\TaxRuleHelper;
use PRECO\Includes\Frontend\Engine\Filters\Query\SalesAnalyticsRuleHelper;
use PRECO\Includes\Frontend\Engine\Filters\Query\UserEventsRuleHelper;
use PRECO\Includes\Frontend\Engine\Filters\Query\CustomerInterestRuleHelper;
use PRECO\Includes\Frontend\Engine\Filters\Query\BoughtTogetherRuleHelper;
use PRECO\Includes\Frontend\Core\Engine\ProductRelationsResolver;
use PRECO\Includes\Preco;

defined( 'ABSPATH' ) || exit;

/**
 * Query builder for recommendations engine.
 */
class QueryBuilder {

	/**
	 * Sorting helper.
	 *
	 * @var SortingHelper
	 */
	private $sorting;

	/**
	 * Current-context tax rule helper.
	 *
	 * @var CurrentTaxRuleHelper
	 */
	private $current_tax_rules;

	/**
	 * Gate rule helper.
	 *
	 * @var GateRuleHelper
	 */
	private $gate_rules;

	/**
	 * Tax rule helper.
	 *
	 * @var TaxRuleHelper
	 */
	private $tax_rules;

	/**
	 * Include/exclude rule helper.
	 *
	 * @var IncludeExcludeRuleHelper
	 */
	private $include_exclude_rules;

	/**
	 * Meta rule helper.
	 *
	 * @var MetaRuleHelper
	 */
	private $meta_rules;

	/**
	 * Date rule helper.
	 *
	 * @var DateRuleHelper
	 */
	private $date_rules;

	/**
	 * Context resolver.
	 *
	 * @var ContextResolver
	 */
	private $context_resolver;

	/**
	 * Constraints finalizer.
	 *
	 * @var ConstraintsFinalizer
	 */
	private $constraints_finalizer;

	/**
	 * WooCommerce built-in source resolver.
	 *
	 * @var ProductRelationsResolver
	 */
	private $product_relations_resolver;

	/**
	 * Sales analytics rule helper.
	 *
	 * @var SalesAnalyticsRuleHelper
	 */
	private $sales_analytics_rules;

	/**
	 * User events rule helper.
	 *
	 * @var UserEventsRuleHelper
	 */
	private $user_events_rules;

	/**
	 * Customer interest rule helper.
	 *
	 * @var CustomerInterestRuleHelper
	 */
	private $customer_interest_rules;

	/**
	 * Bought-together rule helper.
	 *
	 * @var BoughtTogetherRuleHelper
	 */
	private $bought_together_rules;

	/**
	 * Constructor.
	 *
	 * @param SortingHelper $sorting Sorting helper.
	 * @param PriceHelper   $price   Price helper.
	 */
	public function __construct( SortingHelper $sorting, PriceHelper $price ) {
		$this->sorting                    = $sorting;
		$this->current_tax_rules          = new CurrentTaxRuleHelper();
		$this->gate_rules                 = new GateRuleHelper();
		$this->tax_rules                  = new TaxRuleHelper();
		$this->include_exclude_rules      = new IncludeExcludeRuleHelper();
		$this->meta_rules                 = new MetaRuleHelper( $price );
		$this->date_rules                 = new DateRuleHelper();
		$this->context_resolver           = new ContextResolver();
		$this->constraints_finalizer      = new ConstraintsFinalizer();
		$this->product_relations_resolver = new ProductRelationsResolver();
		$this->sales_analytics_rules      = new SalesAnalyticsRuleHelper();
		$this->user_events_rules          = new UserEventsRuleHelper();
		$this->customer_interest_rules    = new CustomerInterestRuleHelper();
		$this->bought_together_rules      = new BoughtTogetherRuleHelper();
	}

	/**
	 * Build WP_Query args from rules.
	 *
	 * @param array $rules Rules.
	 * @param array $sorting Sorting.
	 * @param array $context Context.
	 * @param int   $limit Limit.
	 * @return array
	 */
	public function build_query_args( $rules, $sorting, $context, $limit ) {
		if ( ! function_exists( 'wc_get_product' ) ) {
			return array();
		}

		$ctx         = $this->context_resolver->resolve( $context );
		$product_id  = $ctx['product_id'];
		$product     = $ctx['product'];
		$exclude_ids = $ctx['exclude_ids'];
		$user_id     = get_current_user_id();

		// 'all' = AND across filters (default), 'any' = OR across filters.
		$match_mode     = isset( $context['match_mode'] ) && 'any' === (string) $context['match_mode'] ? 'any' : 'all';
		$query_relation = ( 'any' === $match_mode ) ? 'OR' : 'AND';

		$tax_query  = array();
		$meta_query = array();
		$date_query = array();

		$include_ids   = null; // null means no constraint.
		$enforce_empty = false;

		foreach ( $rules as $rule ) {
			if ( ! is_array( $rule ) ) {
				continue;
			}
			if ( isset( $rule['enabled'] ) && false === $rule['enabled'] ) {
				continue;
			}
			$trigger_type = isset( $rule['triggerType'] ) ? (string) $rule['triggerType'] : '';
			$relation     = isset( $rule['relation'] ) ? (string) $rule['relation'] : 'in';
			$value        = $rule['value'] ?? null;

			// Pro-only trigger types must never run without an active license,
			// even if the rule was saved before a downgrade/expiry. Skip just
			// this rule — the remaining free-tier rules still apply.
			if ( Preco::is_pro_trigger_type( $trigger_type ) && ! Preco::is_lc_active() ) {
				continue;
			}

			switch ( $trigger_type ) {
				case 'current_attribute':
					$this->current_tax_rules->add_attribute_rule( $tax_query, $enforce_empty, $relation, $value, $product );
					if ( $enforce_empty ) {
						break 2;
					}
					break;
				case 'current_category':
					$this->current_tax_rules->add_rule( $tax_query, $enforce_empty, 'product_cat', $relation, $value, $product );
					if ( $enforce_empty ) {
						break 2;
					}
					break;
				case 'current_tag':
					$this->current_tax_rules->add_rule( $tax_query, $enforce_empty, 'product_tag', $relation, $value, $product );
					if ( $enforce_empty ) {
						break 2;
					}
					break;
				case 'category':
					$this->tax_rules->add_tax_rule( $tax_query, 'product_cat', $relation, $value );
					break;
				case 'tag':
					$this->tax_rules->add_tax_rule( $tax_query, 'product_tag', $relation, $value );
					break;
				case 'brand':
					$tax = $this->tax_rules->resolve_brand_taxonomy();
					if ( $tax ) {
						$this->tax_rules->add_tax_rule( $tax_query, $tax, $relation, $value );
					}
					break;
				case 'attribute':
					$this->tax_rules->add_attribute_rule( $tax_query, $relation, $value );
					break;
				case 'featured':
					$this->tax_rules->add_featured_rule( $tax_query, $relation );
					break;
				case 'on_sale':
					$this->include_exclude_rules->add_on_sale_rule( $include_ids, $exclude_ids, $relation );
					break;
				case 'specific_products':
					$this->include_exclude_rules->add_product_rule( $include_ids, $exclude_ids, $relation, $value );
					break;
				case 'recently_viewed':
					$this->include_exclude_rules->add_recently_viewed_rule( $include_ids, $exclude_ids, $relation );
					break;
				case 'related':
				case 'upsells':
				case 'cross_sells':
					$wc_ids = $this->product_relations_resolver->resolve( $trigger_type, $ctx, 9999 );
					$this->include_exclude_rules->add_wc_source_rule( $include_ids, $exclude_ids, $wc_ids, $relation );
					break;
				case 'stock_status':
					$this->meta_rules->add_stock_status_rule( $meta_query, $relation, $value );
					break;
				case 'product_price':
					$this->meta_rules->add_price_rule( $meta_query, $relation, $value );
					break;
				case 'relative_price':
					$this->meta_rules->add_relative_price_rule( $meta_query, $relation, $value, $product_id );
					break;
				case 'created':
					$this->date_rules->add_created_rule( $date_query, $value );
					break;
				case 'best_selling':
					$this->sales_analytics_rules->add_best_selling_rule( $include_ids, $enforce_empty, $relation, $value );
					if ( $enforce_empty ) {
						break 2;
					}
					break;
				case 'high_revenue':
					$this->sales_analytics_rules->add_high_revenue_rule( $include_ids, $enforce_empty, $relation, $value );
					if ( $enforce_empty ) {
						break 2;
					}
					break;
				case 'low_selling':
					$this->sales_analytics_rules->add_low_selling_rule( $include_ids, $enforce_empty, $relation, $value );
					if ( $enforce_empty ) {
						break 2;
					}
					break;
				case 'high_conversion':
					$this->sales_analytics_rules->add_high_conversion_rule( $include_ids, $enforce_empty, $relation, $value );
					if ( $enforce_empty ) {
						break 2;
					}
					break;
				case 'high_click_through':
					$this->sales_analytics_rules->add_high_click_through_rule( $include_ids, $enforce_empty, $relation, $value );
					if ( $enforce_empty ) {
						break 2;
					}
					break;
				case 'frequently_viewed':
					$this->user_events_rules->add_frequently_viewed_rule( $include_ids, $enforce_empty, $relation, $value, $user_id );
					if ( $enforce_empty ) {
						break 2;
					}
					break;
				case 'added_to_cart':
					$this->user_events_rules->add_added_to_cart_rule( $include_ids, $enforce_empty, $relation, $value, $user_id );
					if ( $enforce_empty ) {
						break 2;
					}
					break;
				case 'removed_from_cart':
					$this->user_events_rules->add_removed_from_cart_rule( $include_ids, $enforce_empty, $relation, $value, $user_id );
					if ( $enforce_empty ) {
						break 2;
					}
					break;
				case 'previously_purchased':
					$this->user_events_rules->add_previously_purchased_rule( $include_ids, $enforce_empty, $relation, $value, $user_id );
					if ( $enforce_empty ) {
						break 2;
					}
					break;
				case 'products_viewed':
					$this->user_events_rules->add_products_viewed_rule( $include_ids, $enforce_empty, $relation, $value, $user_id );
					if ( $enforce_empty ) {
						break 2;
					}
					break;
				case 'bought_together':
					$this->bought_together_rules->add_bought_together_rule( $include_ids, $enforce_empty, $relation, $value, $product_id );
					if ( $enforce_empty ) {
						break 2;
					}
					break;
				case 'customer_interest_categories':
					$this->customer_interest_rules->add_customer_interest_categories_rule( $include_ids, $enforce_empty, $relation, $value, $user_id );
					if ( $enforce_empty ) {
						break 2;
					}
					break;
				case 'customer_interest_tags':
					$this->customer_interest_rules->add_customer_interest_tags_rule( $include_ids, $enforce_empty, $relation, $value, $user_id );
					if ( $enforce_empty ) {
						break 2;
					}
					break;
				case 'customer_interest_attributes':
					$this->customer_interest_rules->add_customer_interest_attributes_rule( $include_ids, $enforce_empty, $relation, $value, $user_id );
					if ( $enforce_empty ) {
						break 2;
					}
					break;
				default:
					$state       = apply_filters(
						'preco_recommendations_apply_rule',
						array(
							'tax_query'   => $tax_query, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
						'meta_query'      => $meta_query, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
							'date_query'  => $date_query,
							'include_ids' => $include_ids,
							'exclude_ids' => $exclude_ids,
						),
						$rule,
						$context
					);
					$tax_query   = isset( $state['tax_query'] ) && is_array( $state['tax_query'] ) ? $state['tax_query'] : $tax_query;
					$meta_query  = isset( $state['meta_query'] ) && is_array( $state['meta_query'] ) ? $state['meta_query'] : $meta_query;
					$date_query  = isset( $state['date_query'] ) && is_array( $state['date_query'] ) ? $state['date_query'] : $date_query;
					$include_ids = array_key_exists( 'include_ids', $state ) ? $state['include_ids'] : $include_ids;
					$exclude_ids = isset( $state['exclude_ids'] ) && is_array( $state['exclude_ids'] ) ? $state['exclude_ids'] : $exclude_ids;
					break;
			}
		}

		if ( $enforce_empty ) {
			return array( 'post__in' => array( 0 ) );
		}

		$final = $this->constraints_finalizer->finalize( $include_ids, $context );
		if ( $final['enforce_empty'] ) {
			return array( 'post__in' => array( 0 ) );
		}
		$include_ids = $final['include_ids'];

		$orderby = $this->sorting->resolve_orderby( $sorting, $context );
		$order   = $this->sorting->resolve_order( $sorting, $context );

		$args = array(
			'post_type'              => 'product',
			'post_status'            => 'publish',
			'posts_per_page'         => (int) $limit,
			'fields'                 => 'ids',
			'no_found_rows'          => true,
			'ignore_sticky_posts'    => true,
			'orderby'                => $orderby,
			'order'                  => $order,
			'post__not_in'           => $exclude_ids, // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in
			'suppress_filters'       => false,
			'update_post_meta_cache' => false,
			'update_post_term_cache' => false,
		);

		// Defensively strip excluded IDs from include_ids before setting post__in.
		// WooCommerce pre_get_posts hooks can suppress post__not_in when post__in is
		// present, so removing them here is the only reliable guarantee.
		if ( is_array( $include_ids ) && ! empty( $exclude_ids ) ) {
			$include_ids = array_values( array_diff( $include_ids, $exclude_ids ) );
			if ( empty( $include_ids ) ) {
				return array( 'post__in' => array( 0 ) );
			}
		}

		if ( is_array( $include_ids ) ) {
			$args['post__in'] = $include_ids;
		}

		if ( 'meta_value_num' === $orderby ) {
			$meta_key = $this->sorting->resolve_orderby_meta_key( $sorting, $context );
			if ( $meta_key ) {
				$args['meta_key'] = $meta_key; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
			}
		}

		if ( ! empty( $tax_query ) ) {
			// Prepend relation only when multiple clauses exist; WP ignores it for single-clause arrays.
			if ( count( $tax_query ) > 1 ) {
				array_unshift( $tax_query, array( 'relation' => $query_relation ) );
			}
			$args['tax_query'] = $tax_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
		}
		if ( ! empty( $meta_query ) ) {
			if ( count( $meta_query ) > 1 ) {
				array_unshift( $meta_query, array( 'relation' => $query_relation ) );
			}
			$args['meta_query'] = $meta_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
		}
		if ( ! empty( $date_query ) ) {
			$args['date_query'] = $date_query;
		}

		$args = apply_filters( 'preco_recommendations_query_args', $args, $rules, $sorting, $context );
		return is_array( $args ) ? $args : array();
	}
}
