<?php
/**
 * Manual (PHP-level) sorting helpers.
 *
 * @package PRECO
 */

namespace PRECO\Includes\Frontend\Engine\Sorting;

use PRECO\Includes\Frontend\Engine\Filters\PriceHelper;

defined( 'ABSPATH' ) || exit;

/**
 * Manual (PHP-level) sorting helpers.
 */
class ManualSorter {

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

	/**
	 * Price helper.
	 *
	 * @var PriceHelper
	 */
	private $price;

	/**
	 * Constructor.
	 *
	 * @param SortingHelper $sorting Sorting helper.
	 * @param PriceHelper   $price   Price helper.
	 */
	public function __construct( SortingHelper $sorting, PriceHelper $price ) {
		$this->sorting = $sorting;
		$this->price   = $price;
	}

	/**
	 * Decide whether we should apply a deterministic PHP-level sort.
	 *
	 * @param array $sorting Sorting rules.
	 * @param array $context Context.
	 * @return array{trigger:string,dir:string}|array
	 */
	public function resolve_manual_sorting_from_sorting( $sorting, $context ) {
		if ( isset( $context['_preferred_ids'] ) && is_array( $context['_preferred_ids'] ) ) {
			return array();
		}

		$resolved = $this->sorting->pick_sorting_rule( $sorting );
		if ( empty( $resolved ) || ! is_array( $resolved ) ) {
			return array();
		}

		$trigger  = isset( $resolved['triggerType'] ) ? (string) $resolved['triggerType'] : '';
		$relation = isset( $resolved['relation'] ) ? (string) $resolved['relation'] : '';
		$dir      = ( 'low_to_high' === $relation ) ? 'ASC' : 'DESC';

		if ( 'price' === $trigger ) {
			return array(
				'trigger' => 'price',
				'dir'     => $dir,
			);
		}

		return array();
	}

	/**
	 * Sort product IDs by effective WooCommerce price.
	 *
	 * @param int[]  $ids Product IDs.
	 * @param string $dir ASC|DESC.
	 * @return int[]
	 */
	public function sort_ids_by_effective_price( $ids, $dir = 'DESC' ) {
		$ids = is_array( $ids ) ? array_values( array_filter( array_map( 'absint', $ids ) ) ) : array();
		if ( empty( $ids ) ) {
			return array();
		}

		$dir = strtoupper( (string) $dir );
		if ( 'ASC' !== $dir && 'DESC' !== $dir ) {
			$dir = 'DESC';
		}

		$prices = array();
		foreach ( $ids as $pid ) {
			$prices[ $pid ] = $this->price->get_effective_price_for_product_id( $pid );
		}

		usort(
			$ids,
			function ( $a, $b ) use ( $prices, $dir ) {
				$pa = array_key_exists( $a, $prices ) ? $prices[ $a ] : null;
				$pb = array_key_exists( $b, $prices ) ? $prices[ $b ] : null;

				if ( null === $pa && null === $pb ) {
					return $a <=> $b;
				}
				if ( null === $pa ) {
					return 1;
				}
				if ( null === $pb ) {
					return -1;
				}

				if ( $pa === $pb ) {
					return $a <=> $b;
				}

				if ( 'ASC' === $dir ) {
					return ( $pa < $pb ) ? -1 : 1;
				}
				return ( $pa > $pb ) ? -1 : 1;
			}
		);

		return $ids;
	}

	/**
	 * Multi-rule deterministic PHP sorting.
	 *
	 * @param int[] $ids Product IDs.
	 * @param array $sorting Sorting rules (full array from config).
	 * @param array $context Context.
	 * @param array $preferred_rank_maps trigger => [id => rank].
	 * @return int[]
	 */
	public function sort_ids_by_sorting_rules( $ids, $sorting, $context, $preferred_rank_maps = array() ) {
		$ids = is_array( $ids ) ? array_values( array_filter( array_map( 'absint', $ids ) ) ) : array();
		if ( empty( $ids ) ) {
			return array();
		}

		$rules = $this->sorting->get_enabled_sorting_rules( $sorting );
		if ( empty( $rules ) ) {
			return $ids;
		}

		$need_random = false;
		$need_price  = false;
		$need_date   = false;
		$need_meta   = false;
		foreach ( $rules as $r ) {
			$trigger = isset( $r['triggerType'] ) ? (string) $r['triggerType'] : '';
			switch ( $trigger ) {
				case 'random':
					$need_random = true;
					break;
				case 'price':
					$need_price = true;
					break;
				case 'freshness':
					$need_date = true;
					break;
				case 'rating':
				case 'conversion':
				case 'popularity':
					$need_meta = true;
					break;
				default:
					break;
			}
		}

		// Prime meta cache if needed. Price sorting also reads product/variation
		// postmeta per id (PriceHelper -> EffectivePriceResolver), so it needs
		// the same priming as rating/conversion/popularity to avoid one
		// uncached get_post_meta round-trip per candidate product.
		if ( $need_meta || $need_price ) {
			\update_meta_cache( 'post', $ids );
		}

		$rand   = array();
		$prices = array();
		$dates  = array();
		$rating = array();
		$sales  = array();
		foreach ( $ids as $pid ) {
			if ( $need_random ) {
				$rand[ $pid ] = \wp_rand();
			}
			if ( $need_price ) {
				$prices[ $pid ] = $this->price->get_effective_price_for_product_id( $pid );
			}
			if ( $need_date ) {
				$dates[ $pid ] = (int) \get_post_time( 'U', true, $pid );
			}
			if ( $need_meta ) {
				$rating[ $pid ] = (float) \get_post_meta( $pid, '_wc_average_rating', true );
				$sales[ $pid ]  = (int) \get_post_meta( $pid, 'total_sales', true );
			}
		}

		usort(
			$ids,
			function ( $a, $b ) use ( $rules, $preferred_rank_maps, $rand, $prices, $dates, $rating, $sales ) {
				foreach ( $rules as $r ) {
					$trigger  = isset( $r['triggerType'] ) ? (string) $r['triggerType'] : '';
					$relation = isset( $r['relation'] ) ? (string) $r['relation'] : '';

					$av  = null;
					$bv  = null;
					$dir = 'DESC';

					switch ( $trigger ) {
						case 'freshness':
							$av  = $dates[ $a ] ?? 0;
							$bv  = $dates[ $b ] ?? 0;
							$dir = ( 'old_to_new' === $relation ) ? 'ASC' : 'DESC';
							break;
						case 'price':
							$av  = array_key_exists( $a, $prices ) ? $prices[ $a ] : null;
							$bv  = array_key_exists( $b, $prices ) ? $prices[ $b ] : null;
							$dir = ( 'low_to_high' === $relation ) ? 'ASC' : 'DESC';
							break;
						case 'rating':
							$av  = $rating[ $a ] ?? 0.0;
							$bv  = $rating[ $b ] ?? 0.0;
							$dir = ( 'low_to_high' === $relation ) ? 'ASC' : 'DESC';
							break;
						case 'conversion':
							$av  = $sales[ $a ] ?? 0;
							$bv  = $sales[ $b ] ?? 0;
							$dir = ( 'low_to_high' === $relation ) ? 'ASC' : 'DESC';
							break;
						case 'popularity':
							$map      = isset( $preferred_rank_maps['popularity'] ) && is_array( $preferred_rank_maps['popularity'] )
								? $preferred_rank_maps['popularity']
								: array();
							$low_high = ( 'low_to_high' === $relation );

							if ( ! empty( $map ) ) {
								// Rank map is pre-reversed for low_to_high in PreferredIdsProvider,
								// so rank 0 always means "show first" — sort ASC on rank.
								$am = array_key_exists( $a, $map ) ? (int) $map[ $a ] : null;
								$bm = array_key_exists( $b, $map ) ? (int) $map[ $b ] : null;
								if ( null === $am && null === $bm ) {
									break;
								}
								// Unranked products (no order history):
								// High→Low: push to back (return 1 / -1 as before).
								// Low→High: pull to front (invert the return values).
								if ( null === $am ) {
									return $low_high ? -1 : 1;
								}
								if ( null === $bm ) {
									return $low_high ? 1 : -1;
								}
								$av  = $am;
								$bv  = $bm;
								$dir = 'ASC';
							} else {
								// No order-lookup data for this timeframe: fall back to
								// all-time total_sales postmeta, respecting the chosen direction.
								$av  = $sales[ $a ] ?? 0;
								$bv  = $sales[ $b ] ?? 0;
								$dir = $low_high ? 'ASC' : 'DESC';
							}
							break;
						case 'bought_together_frequency':
							$map = isset( $preferred_rank_maps[ $trigger ] ) && is_array( $preferred_rank_maps[ $trigger ] )
								? $preferred_rank_maps[ $trigger ]
								: array();
							$am  = array_key_exists( $a, $map ) ? (int) $map[ $a ] : null;
							$bm  = array_key_exists( $b, $map ) ? (int) $map[ $b ] : null;
							if ( null === $am && null === $bm ) {
								break;
							}
							if ( null === $am ) {
								return 1;
							}
							if ( null === $bm ) {
								return -1;
							}
							$av  = $am;
							$bv  = $bm;
							$dir = 'ASC';
							break;
						case 'random':
							$av  = $rand[ $a ] ?? 0;
							$bv  = $rand[ $b ] ?? 0;
							$dir = 'ASC';
							break;
						default:
							break;
					}

					if ( null === $av && null === $bv ) {
						continue;
					}
					if ( null === $av ) {
						return 1;
					}
					if ( null === $bv ) {
						return -1;
					}

					if ( $av === $bv ) {
						continue;
					}

					if ( 'ASC' === $dir ) {
						return ( $av < $bv ) ? -1 : 1;
					}
					return ( $av > $bv ) ? -1 : 1;
				}

				return $a <=> $b;
			}
		);

		return $ids;
	}
}
