<?php
/**
 * Location renderer.
 *
 * @package PRECO
 */

namespace PRECO\Includes\Frontend\Core\Page;

use PRECO\Includes\Frontend\Core\CartConditions;
use PRECO\Includes\Frontend\Core\RecommendationsCache;
use PRECO\Includes\Frontend\Core\RecommendationsConfig;
use PRECO\Includes\Frontend\Core\RecommendationsEngine;
use PRECO\Includes\Frontend\Core\RecommendationsRenderer;
use PRECO\Includes\Preco;

defined( 'ABSPATH' ) || exit;

/**
 * Location renderer.
 */
class LocationRenderer {
	/**
	 * Recommendations config.
	 *
	 * @var RecommendationsConfig
	 */
	private $config;

	/**
	 * Recommendations engine.
	 *
	 * @var RecommendationsEngine
	 */
	private $engine;

	/**
	 * Recommendations renderer.
	 *
	 * @var RecommendationsRenderer
	 */
	private $renderer;

	/**
	 * Recommendations cache.
	 *
	 * @var RecommendationsCache
	 */
	private $cache;

	/**
	 * Constructor.
	 *
	 * @param RecommendationsConfig   $config   Config.
	 * @param RecommendationsEngine   $engine   Engine.
	 * @param RecommendationsRenderer $renderer Renderer.
	 */
	public function __construct( RecommendationsConfig $config, RecommendationsEngine $engine, RecommendationsRenderer $renderer ) {
		$this->config   = $config;
		$this->engine   = $engine;
		$this->renderer = $renderer;
		$this->cache    = new RecommendationsCache();
	}

	/**
	 * Render recommendations for a location.
	 *
	 * @param string $page_key Page key.
	 * @param string $location_key Location key.
	 * @param array  $hook_args Hook args.
	 * @param array  $context Context.
	 * @return void
	 */
	public function render( $page_key, $location_key, $hook_args, $context ) {
		$loc      = $this->config->get_location( (string) $page_key, (string) $location_key );
		$lists    = isset( $loc['recommendedLists'] ) && is_array( $loc['recommendedLists'] ) ? $loc['recommendedLists'] : array();
		$settings = isset( $loc['settings'] ) && is_array( $loc['settings'] ) ? $loc['settings'] : array();

		if ( isset( $settings['enabled'] ) && false === $settings['enabled'] ) {
			return;
		}
		if ( empty( $lists ) ) {
			return;
		}

		// ---------------------------------------------------------------
		// Display dependency: _always (default) or _sequential.
		// For _sequential, ALL enabled lists are rendered into the page
		// (initially hidden via inline style).  A JS module reads the
		// display rules from the wrapper element, tracks state in
		// localStorage, and shows only the currently active list –
		// including scheduling setTimeout for after_delay transitions.
		// ---------------------------------------------------------------
		// Display Conditions itself is a free feature. Only the After Delay /
		// Page Reloads operators are Pro-only: without an active license,
		// rules using them fall back to Show Instantly.
		$dependency_type = isset( $settings['dependencyType'] ) ? (string) $settings['dependencyType'] : '_always';
		$is_sequential   = '_sequential' === $dependency_type;

		if ( $is_sequential ) {
			$display_rules = isset( $settings['displayRules'] ) && is_array( $settings['displayRules'] ) ? $settings['displayRules'] : array();

			if ( ! Preco::is_lc_active() ) {
				foreach ( $display_rules as &$display_rule ) {
					if ( is_array( $display_rule ) && Preco::is_pro_operator_type( isset( $display_rule['frequency'] ) ? $display_rule['frequency'] : '' ) ) {
						$display_rule['frequency'] = 'show_instantly';
						unset( $display_rule['frequencyValue'] );
					}
				}
				unset( $display_rule );
			}

			echo '<div class="preco-seq-location" data-preco-seq-location data-location-key="' . esc_attr( $location_key ) . '" data-display-rules="' . esc_attr( (string) wp_json_encode( $display_rules ) ) . '">';
		}

		foreach ( $lists as $key => $list ) {
			if ( ! is_array( $list ) ) {
				continue;
			}
			if ( isset( $list['enabled'] ) && false === $list['enabled'] ) {
				continue;
			}

			// Handle Scheduling: if the list has a schedule and the current time doesn't match, skip it.
			if ( ! self::is_schedule_active( isset( $list['schedule'] ) ? $list['schedule'] : array() ) ) {
				continue;
			}

			// Cart Condition (Cart Quantity, Cart Value, Total Spend, etc.) is only
			// a setting for the Cart/Checkout locations — same page-scoping as Popup Upsell.
			if ( in_array( $page_key, array( 'cart', 'checkout' ), true )
				&& ! CartConditions::passes( isset( $list['cartConditions'] ) ? $list['cartConditions'] : array() ) ) {
				continue;
			}

			// ---------------------------------------------------------------
			// Custom (rules-based) lists.
			// ---------------------------------------------------------------
			$rules   = isset( $list['rules'] ) && is_array( $list['rules'] ) ? $list['rules'] : array();
			$sorting = isset( $list['sorting'] ) && is_array( $list['sorting'] ) ? $list['sorting'] : array();

			$rules_filtered   = $this->filter_enabled_items( $rules );
			$sorting_filtered = $this->filter_enabled_items( $sorting );

			// If nothing is configured, don't render a default "all products" query.
			if ( empty( $rules_filtered ) && empty( $sorting_filtered ) ) {
				continue;
			}

			$effective_limit = $this->resolve_list_limit( $list );
			if ( $effective_limit <= 0 ) {
				continue;
			}

			// Inject matchMode so the query builder can apply AND vs OR logic.
			$match_mode   = isset( $list['matchMode'] ) && 'any' === (string) $list['matchMode'] ? 'any' : 'all';
			$list_context = array_merge( $context, array( 'match_mode' => $match_mode ) );

			// Non-deterministic sorting (e.g. "Randomized") must bypass the cache
			// entirely so the result differs on every page load.
			if ( $this->cache->should_skip_cache( $sorting_filtered ) || $this->cache->rules_require_no_cache( $rules_filtered ) ) {
				$product_ids = $this->engine->get_product_ids( $rules_filtered, $sorting_filtered, $list_context, $effective_limit );
			} else {
				$cache_key   = $this->cache->make_key( $page_key, $location_key, (int) $key, $rules_filtered, $sorting_filtered, $list_context, $effective_limit );
				$product_ids = $this->cache->get( $cache_key );

				if ( null === $product_ids ) {
					$product_ids = $this->engine->get_product_ids( $rules_filtered, $sorting_filtered, $list_context, $effective_limit );
					$ttl         = $this->cache->resolve_ttl( $rules_filtered );
					// Cache even empty results to avoid re-running the query for
					// rules that genuinely produce no matches (e.g. gate rules
					// that block on the current product's terms).
					$this->cache->set( $cache_key, is_array( $product_ids ) ? $product_ids : array(), $ttl );
				}
			}

			if ( empty( $product_ids ) ) {
				continue;
			}

			$list['custom_props'] = array(
				'location_key' => $location_key,
				'page_key'     => $page_key,
				'list_id'      => isset( $list['id'] ) ? (string) $list['id'] : '',
				'seq_hidden'   => $is_sequential,
			);

			$this->renderer->render_products( $product_ids, self::build_wrapper_class( $page_key, $location_key, $key ), $list );
		}

		if ( $is_sequential ) {
			echo '</div>';
		}
	}

	/**
	 * Resolve the effective product limit for a list.
	 *
	 * @param array $recommended_list Recommended list config.
	 * @return int
	 */
	private function resolve_list_limit( $recommended_list ) {
		$effective_limit = 4;
		if ( isset( $recommended_list['limit'] ) ) {
			$list_limit = (int) $recommended_list['limit'];
			if ( $list_limit > 0 ) {
				$effective_limit = $list_limit;
			}
		}
		return $effective_limit;
	}

	/**
	 * Check whether a schedule is currently active.
	 *
	 * Returns true when the associated content should be shown, false when
	 * it should be skipped. An absent or empty schedule is treated as
	 * always-active. Public/static so it can be reused by other renderers
	 * (e.g. Popup Upsell) that need the same schedule-window logic against
	 * a schedule that isn't nested inside a page-location "list".
	 *
	 * @param array $schedule Schedule configuration (a recommendedList's `schedule` sub-array).
	 * @return bool
	 */
	public static function is_schedule_active( array $schedule ) {
		if ( empty( $schedule ) ) {
			return true;
		}

		$schedule_mode = isset( $schedule['scheduleMode'] ) ? (string) $schedule['scheduleMode'] : 'right-now';
		$start_date    = isset( $schedule['startDate'] ) && '' !== (string) $schedule['startDate'] ? (string) $schedule['startDate'] : '';
		$start_time    = isset( $schedule['startTime'] ) && '' !== (string) $schedule['startTime'] ? (string) $schedule['startTime'] : '00:00';
		$end_date      = isset( $schedule['endDate'] ) && '' !== (string) $schedule['endDate'] ? (string) $schedule['endDate'] : '';
		$end_time      = isset( $schedule['endTime'] ) && '' !== (string) $schedule['endTime'] ? (string) $schedule['endTime'] : '23:59';

		$wp_tz = wp_timezone();
		$now   = new \DateTime( 'now', $wp_tz );

		// Enforce start boundary only when scheduleMode is 'start-later' and a startDate is provided.
		if ( 'start-later' === $schedule_mode && '' !== $start_date ) {
			$start_dt = \DateTime::createFromFormat( 'Y-m-d H:i', $start_date . ' ' . $start_time, $wp_tz );
			if ( $start_dt && $now < $start_dt ) {
				return false;
			}
		}

		// Enforce end boundary whenever an endDate is provided.
		if ( '' !== $end_date ) {
			$end_dt = \DateTime::createFromFormat( 'Y-m-d H:i', $end_date . ' ' . $end_time, $wp_tz );
			if ( $end_dt ) {
				// Treat the end minute as fully inclusive (e.g. 17:56 → 17:56:59).
				$end_dt->setTime( (int) $end_dt->format( 'H' ), (int) $end_dt->format( 'i' ), 59 );
				if ( $now > $end_dt ) {
					return false;
				}
			}
		}

		return true;
	}

	/**
	 * Filter out items explicitly marked as disabled.
	 *
	 * @param array $items Items to filter.
	 * @return array
	 */
	private function filter_enabled_items( $items ) {
		if ( empty( $items ) || ! is_array( $items ) ) {
			return array();
		}
		$out = array();
		foreach ( $items as $item ) {
			if ( ! is_array( $item ) ) {
				continue;
			}
			if ( isset( $item['enabled'] ) && false === $item['enabled'] ) {
				continue;
			}
			$out[] = $item;
		}
		return $out;
	}

	/**
	 * Build the wrapper CSS classes for a rendered list.
	 *
	 * Public/static so PreviewRenderer can reuse it for identical markup in
	 * the admin builder's live preview.
	 *
	 * @param string $page_key Page key.
	 * @param string $location_key Location key.
	 * @param int    $index List index within the location.
	 * @return string
	 */
	public static function build_wrapper_class( $page_key, $location_key, $index ) {
		$page_key     = sanitize_html_class( (string) $page_key );
		$location_key = sanitize_html_class( (string) $location_key );
		$i            = sanitize_html_class( (string) ( (int) $index + 1 ) );
		return 'preco-recommendations--' . $page_key .
			' preco-recommendations--' . $location_key .
			' preco-recommendations--' . $location_key . '-list--' . $i;
	}
}
