<?php
/**
 * Preferred IDs provider (smart sorts like co-purchase and popularity).
 *
 * @package PRECO
 */

namespace PRECO\Includes\Frontend\Engine\Sorting;

use PRECO\Includes\Frontend\Engine\Sorting\Preferred\TimeframeHelper;
use PRECO\Includes\Frontend\Engine\Sorting\Preferred\WcOrderLookupRepository;

defined( 'ABSPATH' ) || exit;

/**
 * Preferred IDs provider (smart sorts like co-purchase and popularity).
 */
class PreferredIdsProvider {

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

	/**
	 * WooCommerce order lookup repository.
	 *
	 * @var WcOrderLookupRepository
	 */
	private $repo;

	/**
	 * Timeframe helper.
	 *
	 * @var TimeframeHelper
	 */
	private $timeframes;

	/**
	 * Constructor.
	 *
	 * @param SortingHelper $sorting Sorting helper.
	 */
	public function __construct( SortingHelper $sorting ) {
		$this->sorting    = $sorting;
		$this->repo       = new WcOrderLookupRepository();
		$this->timeframes = new TimeframeHelper();
	}

	/**
	 * Optional: get a preferred product ID order based on sorting type.
	 * This enables timeframe-based popularity/conversion and co-purchase sorts.
	 *
	 * @param array $sorting Sorting array.
	 * @param array $context Context.
	 * @param int   $limit Limit.
	 * @return int[]
	 */
	public function get_preferred_ids_from_sorting( $sorting, $context, $limit ) {
		$selected = $this->sorting->pick_sorting_rule( $sorting );
		if ( empty( $selected ) || ! is_array( $selected ) ) {
			return array();
		}

		$trigger  = isset( $selected['triggerType'] ) ? (string) $selected['triggerType'] : '';
		$relation = isset( $selected['relation'] ) ? (string) $selected['relation'] : '';
		$value    = isset( $selected['value'] ) ? $selected['value'] : null;

		$fetch = max( 50, (int) $limit * 10 );

		if ( 'bought_together_frequency' === $trigger ) {
			$product_id = isset( $context['product_id'] ) ? absint( $context['product_id'] ) : 0;
			if ( ! $product_id ) {
				return array();
			}
			list( $since, $bucket ) = $this->resolve_co_purchase_window( is_string( $value ) ? $value : '' );
			return $this->get_co_purchased_product_ids( $product_id, $fetch, $since, $bucket );
		}

		if ( 'popularity' === $trigger ) {
			$timeframe = is_string( $value ) ? (string) $value : '';
			$seconds   = $this->timeframes->to_seconds( $timeframe );
			if ( -1 === $seconds ) {
				$ids = $this->get_top_selling_product_ids_all_time( $fetch );
			} elseif ( $seconds > 0 ) {
				$ids = $this->get_top_selling_product_ids_since( time() - $seconds, $fetch, $timeframe );
			} else {
				$ids = $this->get_top_selling_product_ids_all_time( $fetch );
			}
			if ( 'low_to_high' === $relation ) {
				$ids = array_reverse( $ids );
			}
			return $ids;
		}

		return array();
	}

	/**
	 * Resolve the "since" timestamp and cache bucket for a merchant-selected timeframe.
	 *
	 * Empty/invalid timeframe means "all time".
	 *
	 * @param string $timeframe Timeframe key (e.g. 'last_7d'), or '' for all time.
	 * @return array{0:int,1:string} [since unix timestamp (0 = all time), cache bucket label]
	 */
	private function resolve_co_purchase_window( $timeframe ) {
		if ( is_string( $timeframe ) && '' !== $timeframe ) {
			$seconds = $this->timeframes->to_seconds( $timeframe );
			if ( $seconds > 0 ) {
				return array( time() - $seconds, $timeframe );
			}
		}
		return array( 0, 'alltime' );
	}

	/**
	 * Get co-purchased product IDs, optionally restricted to orders since a given time.
	 *
	 * @param int    $product_id Product ID.
	 * @param int    $limit Limit.
	 * @param int    $since Unix timestamp; 0 = all time.
	 * @param string $cache_bucket Cache bucket label (timeframe key or 'alltime').
	 * @return int[]
	 */
	private function get_co_purchased_product_ids( $product_id, $limit, $since = 0, $cache_bucket = 'alltime' ) {
		$product_id = absint( $product_id );
		$limit      = (int) $limit;
		$since      = (int) $since;
		if ( $product_id <= 0 || $limit <= 0 ) {
			return array();
		}

		$cache_key = 'preco_copurchased_' . $cache_bucket . '_' . $product_id . '_' . $limit;
		$cached    = \get_transient( $cache_key );
		if ( is_array( $cached ) ) {
			return array_slice( array_values( array_filter( array_map( 'absint', $cached ) ) ), 0, $limit );
		}

		$ids = $this->repo->get_co_purchased_product_ids( $product_id, $limit, $since );
		$ids = array_values( array_unique( array_filter( array_map( 'absint', $ids ) ) ) );
		if ( empty( $ids ) ) {
			\set_transient( $cache_key, array(), HOUR_IN_SECONDS );
			return array();
		}

		\set_transient( $cache_key, $ids, 6 * HOUR_IN_SECONDS );
		return array_slice( $ids, 0, $limit );
	}

	/**
	 * Get top-selling products all-time.
	 *
	 * @param int $limit Limit.
	 * @return int[]
	 */
	private function get_top_selling_product_ids_all_time( $limit ) {
		$limit = (int) $limit;
		if ( $limit <= 0 ) {
			return array();
		}

		$cache_key = 'preco_topselling_all_' . $limit;
		$cached    = \get_transient( $cache_key );
		if ( is_array( $cached ) ) {
			return array_values( array_filter( array_map( 'absint', $cached ) ) );
		}

		$ids = $this->repo->get_top_selling_product_ids_all_time( $limit );
		$ids = array_values( array_unique( array_filter( array_map( 'absint', $ids ) ) ) );
		if ( empty( $ids ) ) {
			\set_transient( $cache_key, array(), HOUR_IN_SECONDS );
			return array();
		}

		\set_transient( $cache_key, $ids, HOUR_IN_SECONDS );
		return $ids;
	}

	/**
	 * Build preferred rank maps for all enabled sorting rules.
	 * Map: trigger => [product_id => rank], where rank 0 is most preferred.
	 *
	 * @param array $sorting Sorting array.
	 * @param array $context Context.
	 * @param int   $limit Limit.
	 * @return array<string,array<int,int>>
	 */
	public function get_preferred_rank_maps_from_sorting( $sorting, $context, $limit ) {
		$rules = $this->sorting->get_enabled_sorting_rules( $sorting );
		if ( empty( $rules ) ) {
			return array();
		}

		$maps = array();
		foreach ( $rules as $rule ) {
			if ( ! is_array( $rule ) ) {
				continue;
			}
			$trigger = isset( $rule['triggerType'] ) ? (string) $rule['triggerType'] : '';
			if ( ! $trigger || isset( $maps[ $trigger ] ) ) {
				continue;
			}

			if ( 'bought_together_frequency' === $trigger ) {
				$product_id = isset( $context['product_id'] ) ? absint( $context['product_id'] ) : 0;
				if ( ! $product_id ) {
					continue;
				}
				$timeframe              = isset( $rule['value'] ) && is_string( $rule['value'] ) ? (string) $rule['value'] : '';
				list( $since, $bucket ) = $this->resolve_co_purchase_window( $timeframe );
				$ids                    = $this->get_co_purchased_product_ids( $product_id, max( 50, (int) $limit * 10 ), $since, $bucket );
				$maps[ $trigger ]       = $this->to_rank_map( $ids );
				continue;
			}

			if ( 'popularity' === $trigger ) {
				$relation  = isset( $rule['relation'] ) ? (string) $rule['relation'] : '';
				$timeframe = isset( $rule['value'] ) && is_string( $rule['value'] ) ? (string) $rule['value'] : '';
				$seconds   = $this->timeframes->to_seconds( $timeframe );
				if ( -1 === $seconds ) {
					$ids = $this->get_top_selling_product_ids_all_time( max( 50, (int) $limit * 20 ) );
				} elseif ( $seconds > 0 ) {
					$ids = $this->get_top_selling_product_ids_since( time() - $seconds, max( 50, (int) $limit * 20 ), $timeframe );
				} else {
					$ids = $this->get_top_selling_product_ids_all_time( max( 50, (int) $limit * 20 ) );
				}
				if ( 'low_to_high' === $relation ) {
					$ids = array_reverse( $ids );
				}
				$maps[ $trigger ] = $this->to_rank_map( $ids );
				continue;
			}
		}

		return $maps;
	}

	/**
	 * Convert an ordered ID list into a map of ID => rank (0-based position).
	 *
	 * @param int[] $ids IDs.
	 * @return array<int,int>
	 */
	private function to_rank_map( $ids ) {
		$ids = is_array( $ids ) ? array_values( array_filter( array_map( 'absint', $ids ) ) ) : array();
		$map = array();
		foreach ( $ids as $i => $pid ) {
			$map[ (int) $pid ] = (int) $i;
		}
		return $map;
	}

	/**
	 * Get top-selling products since a timestamp.
	 *
	 * @param int    $since Unix timestamp.
	 * @param int    $limit Limit.
	 * @param string $cache_bucket Stable cache bucket label (timeframe key), used instead of the
	 *                             ever-changing timestamp so the transient can actually be reused.
	 * @return int[]
	 */
	private function get_top_selling_product_ids_since( $since, $limit, $cache_bucket = '' ) {
		$since = (int) $since;
		$limit = (int) $limit;
		if ( $since <= 0 || $limit <= 0 ) {
			return array();
		}

		$cache_bucket = '' !== $cache_bucket ? $cache_bucket : (string) $since;
		$cache_key    = 'preco_topselling_' . $cache_bucket . '_' . $limit;
		$cached       = \get_transient( $cache_key );
		if ( is_array( $cached ) ) {
			return array_values( array_filter( array_map( 'absint', $cached ) ) );
		}

		$ids = $this->repo->get_top_selling_product_ids_since( $since, $limit );
		$ids = array_values( array_unique( array_filter( array_map( 'absint', $ids ) ) ) );
		if ( empty( $ids ) ) {
			\set_transient( $cache_key, array(), HOUR_IN_SECONDS );
			return array();
		}

		\set_transient( $cache_key, $ids, HOUR_IN_SECONDS );
		return $ids;
	}
}
