<?php

/**
 * Resolves which single Upsell Offer, if any, applies to a given product based on its Scope and Conditions.
 *
 * @link       https://nudgesells.com
 * @since      1.0.0
 *
 * @package    NudgeSells
 * @subpackage NudgeSells/includes
 */

if (! defined('ABSPATH')) {
	exit;
}

/**
 * NudgeSells_Offer_Repository
 *
 * Matches a product against every published offer's scope and conditions and resolves the single best match.
 *
 * @since      1.0.0
 * @package    NudgeSells
 * @subpackage NudgeSells/includes
 * @author     NudgeSells <support@nudgesells.com>
 */
class NudgeSells_Offer_Repository
{

	private static $cache = array();

	/**
	 * Get offer for product.
	 *
	 * @param mixed $product_id
	 * @param int $variation_id
	 *
	 * @return mixed
	 * @since 1.0.0
	 */
	public static function get_offer_for_product($product_id, $variation_id = 0)
	{
		$cache_key = $product_id . ':' . $variation_id . ':' . NudgeSells_Multilingual::current_language();
		if (isset(self::$cache[$cache_key])) {
			return self::$cache[$cache_key];
		}

		$offers = self::query_offers(false);

		$match = self::find_best_match($offers, $product_id, $variation_id);

		if (! $match && NudgeSells_Multilingual::is_active()) {
			$all_offers = self::query_offers(true);
			$fallback      = self::find_best_match($all_offers, $product_id, $variation_id);
			if ($fallback) {
				$translated_id = NudgeSells_Multilingual::get_translated_offer_id($fallback->ID);
				$match         = ($translated_id && $translated_id !== $fallback->ID) ? get_post($translated_id) : $fallback;
			}
		}

		if ($match) {

			$resolved_id = apply_filters('nudgesells_resolve_offer_id', $match->ID);
			if ($resolved_id !== $match->ID) {
				$variant_post = get_post($resolved_id);

				if ($variant_post && 'publish' === $variant_post->post_status) {
					$match = $variant_post;
				}
			}
		}

		self::$cache[$cache_key] = $match;
		return $match;
	}

	/**
	 * Query offers.
	 *
	 * @param mixed $suppress_filters
	 *
	 * @return mixed
	 * @since 1.0.0
	 */
	private static function query_offers($suppress_filters)
	{
		return get_posts(array(
			'post_type'        => NudgeSells_CPT::POST_TYPE,
			'post_status'      => 'publish',
			'posts_per_page'   => -1,
			'no_found_rows'    => true,
			'suppress_filters' => $suppress_filters,
		));
	}

	/**
	 * Find best match.
	 *
	 * @param array $offers
	 * @param mixed $product_id
	 * @param mixed $variation_id
	 *
	 * @return mixed
	 * @since 1.0.0
	 */
	private static function find_best_match(array $offers, $product_id, $variation_id)
	{
		$matches = array();
		foreach ($offers as $offer) {
			if (self::matches_scope($offer->ID, $product_id, $variation_id) && self::matches_conditions($offer->ID)) {
				$matches[] = $offer;
			}
		}

		if (empty($matches)) {
			return null;
		}

		usort($matches, function ($a, $b) use ($variation_id) {
			if ($variation_id) {
				$a_specific = self::scope_targets_variation($a->ID, $variation_id);
				$b_specific = self::scope_targets_variation($b->ID, $variation_id);
				if ($a_specific !== $b_specific) {
					return $a_specific ? -1 : 1;
				}
			}
			$pa = (int) (get_post_meta($a->ID, '_nudgesells_priority', true) ?: 10);
			$pb = (int) (get_post_meta($b->ID, '_nudgesells_priority', true) ?: 10);
			if ($pa === $pb) {
				return $b->ID <=> $a->ID;
			}
			return $pa <=> $pb;
		});

		return $matches[0];
	}

	/**
	 * Scope targets variation.
	 *
	 * @param mixed $offer_id
	 * @param mixed $variation_id
	 *
	 * @return mixed
	 * @since 1.0.0
	 */
	private static function scope_targets_variation($offer_id, $variation_id)
	{
		$scope = self::get_scope($offer_id);
		return 'products' === $scope['type'] && in_array((int) $variation_id, $scope['ids'], true);
	}

	/**
	 * Matches scope.
	 *
	 * @param mixed $offer_id
	 * @param mixed $product_id
	 * @param int $variation_id
	 *
	 * @return mixed
	 * @since 1.0.0
	 */
	public static function matches_scope($offer_id, $product_id, $variation_id = 0)
	{
		$scope = self::get_scope($offer_id);

		$excludes = $scope['exclude_ids'];
		if (in_array((int) $product_id, $excludes, true) || ($variation_id && in_array((int) $variation_id, $excludes, true))) {
			return false;
		}

		switch ($scope['type']) {
			case 'all':
				return true;

			case 'products':

				return in_array((int) $product_id, $scope['ids'], true)
					|| ($variation_id && in_array((int) $variation_id, $scope['ids'], true));

			case 'categories':
				return has_term(array_map('absint', $scope['ids']), 'product_cat', $product_id);

			case 'tags':
				return has_term(array_map('absint', $scope['ids']), 'product_tag', $product_id);
		}

		return false;
	}

	/**
	 * Convert a plain Y-m-d date string (as entered in the Conditions
	 * date fields) into a true Unix timestamp, interpreting the date as
	 * being in WordPress's configured local timezone (Settings → General)
	 *
	 * @param string $date_string
	 * @param string $time_string
	 *
	 * @return int|false
	 * @since 1.0.0
	 */
	public static function local_date_to_timestamp($date_string, $time_string = '00:00:00')
	{
		if (empty($date_string)) {
			return false;
		}

		$gmt_date_string = get_gmt_from_date($date_string . ' ' . $time_string);
		if (! $gmt_date_string) {
			return false;
		}

		$timestamp = strtotime($gmt_date_string . ' GMT');
		return $timestamp ? $timestamp : false;
	}

	/**
	 * Matches conditions.
	 *
	 * @param mixed $offer_id
	 *
	 * @return bool
	 * @since 1.0.0
	 */
	public static function matches_conditions($offer_id)
	{
		$user_condition = get_post_meta($offer_id, '_nudgesells_condition_user', true) ?: 'everyone';
		if ('logged_in' === $user_condition && ! is_user_logged_in()) {
			return false;
		}
		if ('logged_out' === $user_condition && is_user_logged_in()) {
			return false;
		}

		$is_pro = NudgeSells_Pro_Loader::is_active();

		if ($is_pro) {
			$start = get_post_meta($offer_id, '_nudgesells_condition_start', true);
			$end   = get_post_meta($offer_id, '_nudgesells_condition_end', true);
			$now   = time();

			$start_ts = self::local_date_to_timestamp($start);
			if ($start_ts && $start_ts > $now) {
				return false;
			}
			$end_ts = self::local_date_to_timestamp($end, '23:59:59');
			if ($end_ts && $end_ts < $now) {
				return false;
			}

			$roles = get_post_meta($offer_id, '_nudgesells_condition_roles', true);
			if (is_array($roles) && ! empty($roles)) {
				if (! is_user_logged_in()) {
					return false;
				}
				$user = wp_get_current_user();
				if (! array_intersect($roles, (array) $user->roles)) {
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Get min qty.
	 *
	 * @param mixed $offer_id
	 *
	 * @return mixed
	 * @since 1.0.0
	 */
	public static function get_min_qty($offer_id)
	{
		if (! NudgeSells_Pro_Loader::is_active()) {
			return 1;
		}
		$min = (int) get_post_meta($offer_id, '_nudgesells_condition_min_qty', true);
		return $min > 0 ? $min : 1;
	}

	/**
	 * Meets min qty.
	 *
	 * @param mixed $offer_id
	 * @param mixed $qty
	 *
	 * @return mixed
	 * @since 1.0.0
	 */
	public static function meets_min_qty($offer_id, $qty)
	{
		return (int) $qty >= self::get_min_qty($offer_id);
	}

	/**
	 * Get scope.
	 *
	 * @param mixed $offer_id
	 *
	 * @return array
	 * @since 1.0.0
	 */
	public static function get_scope($offer_id)
	{
		$ids         = get_post_meta($offer_id, '_nudgesells_scope_ids', true);
		$exclude_ids = get_post_meta($offer_id, '_nudgesells_exclude_ids', true);
		return array(
			'type'        => get_post_meta($offer_id, '_nudgesells_scope_type', true) ?: 'products',

			'ids'         => is_array($ids) ? array_map('absint', $ids) : array(),
			'exclude_ids' => is_array($exclude_ids) ? array_map('absint', $exclude_ids) : array(),
		);
	}

	/**
	 * Describe scope.
	 *
	 * @param mixed $scope
	 *
	 * @return mixed
	 * @since 1.0.0
	 */
	public static function describe_scope($scope)
	{
		switch ($scope['type']) {
			case 'all':
				return __('All products', 'nudgesells');
			case 'products':
				/* translators: %d: number of specific products this offer is scoped to */
				return sprintf(_n('%d product', '%d products', count($scope['ids']), 'nudgesells'), count($scope['ids']));
			case 'categories':
				/* translators: %d: number of categories this offer is scoped to */
				return sprintf(_n('%d category', '%d categories', count($scope['ids']), 'nudgesells'), count($scope['ids']));
			case 'tags':
				/* translators: %d: number of tags this offer is scoped to */
				return sprintf(_n('%d tag', '%d tags', count($scope['ids']), 'nudgesells'), count($scope['ids']));
		}
		return '-';
	}

	/**
	 * Flush cache.
	 *
	 * @return void
	 * @since 1.0.0
	 */
	public static function flush_cache()
	{
		self::$cache = array();
	}
}
