<?php
/**
 * Preview mode helpers for the admin builder's live iframe.
 *
 * @package PRECO
 */

namespace PRECO\Includes\Frontend\Core;

defined( 'ABSPATH' ) || exit;

/**
 * Detects builder preview requests and serves the draft/sample data they render.
 */
class PreviewMode {

	/**
	 * Transient prefix for a user's draft recommendations snapshot.
	 */
	const DRAFT_TRANSIENT_PREFIX = 'preco_preview_';

	/**
	 * Draft transient TTL. Hygiene only — is_active_request() also requires
	 * the preco_preview query flag, so a stale transient can't affect an
	 * admin's normal (non-preview) storefront browsing.
	 */
	const DRAFT_TTL = 15 * MINUTE_IN_SECONDS;

	/**
	 * Cart-item-data flag marking a cart line added by the preview cart-fill endpoint.
	 */
	const CART_ITEM_FLAG = 'preco_preview_item';

	/**
	 * Order-meta flag marking an order created by the preview Thank You fallback.
	 */
	const ORDER_META_FLAG = '_preco_preview_order';

	/**
	 * Per-request memoization for get_sample_products(), keyed by count.
	 *
	 * @var array<int, \WC_Product[]>
	 */
	private static $sample_products_cache = array();

	/**
	 * Per-request memoization for get_dummy_products(), keyed by count.
	 *
	 * @var array<int, DummyProduct[]>
	 */
	private static $dummy_products_cache = array();

	/**
	 * Whether the current request is a capability-gated builder preview request.
	 *
	 * Two independent gates — the query flag and the capability check — mean
	 * this can never be true for an anonymous shopper or leak into an admin's
	 * normal (non-preview) storefront browsing.
	 *
	 * @return bool
	 */
	public static function is_active_request() {
		// Read-only gate on rendered content, not a state-changing action — no nonce needed.
		if ( ! isset( $_GET['preco_preview'] ) || '1' !== $_GET['preco_preview'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			return false;
		}
		if ( ! is_user_logged_in() ) {
			return false;
		}
		return current_user_can( 'manage_woocommerce' ) || current_user_can( 'manage_options' );
	}

	/**
	 * Whether preview mode should query real, rule-matched store products
	 * (PreviewRenderer) instead of always showing placeholder DummyProduct
	 * cards. Governed by the PRECO_PREVIEW_MODE constant defined in the main
	 * plugin file — 'real' (default) or 'dummy' to force the old
	 * always-placeholder behavior, useful for comparing the two.
	 *
	 * @return bool
	 */
	public static function use_real_products() {
		$mode = defined( 'PRECO_PREVIEW_MODE' ) ? (string) PRECO_PREVIEW_MODE : 'real';
		return 'dummy' !== $mode;
	}

	/**
	 * Transient key for a user's draft snapshot.
	 *
	 * @param int $user_id Optional user ID, defaults to the current user.
	 * @return string
	 */
	public static function draft_key( $user_id = 0 ) {
		$user_id = $user_id ? (int) $user_id : get_current_user_id();
		return self::DRAFT_TRANSIENT_PREFIX . $user_id;
	}

	/**
	 * Persist a draft recommendations snapshot for the current user.
	 *
	 * @param array $recommendations Full recommendations tree (same shape as the saved option).
	 * @return void
	 */
	public static function save_draft_snapshot( array $recommendations ) {
		set_transient( self::draft_key(), $recommendations, self::DRAFT_TTL );
	}

	/**
	 * Clear the current user's draft snapshot.
	 *
	 * @return void
	 */
	public static function clear_draft_snapshot() {
		delete_transient( self::draft_key() );
	}

	/**
	 * Read the current user's draft snapshot.
	 *
	 * @return array
	 */
	public static function get_draft_snapshot() {
		$draft = get_transient( self::draft_key() );
		return is_array( $draft ) ? $draft : array();
	}

	/**
	 * Get the draft location config for a page/location, or an empty shape.
	 *
	 * @param string $page_key Page key.
	 * @param string $location_key Location key.
	 * @return array{settings:array,recommendedLists:array}
	 */
	public static function get_draft_location( $page_key, $location_key ) {
		$draft     = self::get_draft_snapshot();
		$page      = isset( $draft[ $page_key ] ) && is_array( $draft[ $page_key ] ) ? $draft[ $page_key ] : array();
		$locations = isset( $page['locations'] ) && is_array( $page['locations'] ) ? $page['locations'] : array();
		$loc       = isset( $locations[ $location_key ] ) && is_array( $locations[ $location_key ] ) ? $locations[ $location_key ] : array();

		return array(
			'settings'         => isset( $loc['settings'] ) && is_array( $loc['settings'] ) ? $loc['settings'] : array(),
			'recommendedLists' => isset( $loc['recommendedLists'] ) && is_array( $loc['recommendedLists'] ) ? array_values( $loc['recommendedLists'] ) : array(),
		);
	}

	/**
	 * Batch size used to page through the catalog in get_sample_products().
	 */
	const SAMPLE_PRODUCTS_BATCH_SIZE = 20;

	/**
	 * Safety cap on how many products get_sample_products() will scan before
	 * giving up, so a huge catalog with few/no qualifying products can't turn
	 * a preview load into a slow full-table scan.
	 */
	const SAMPLE_PRODUCTS_MAX_SCAN = 500;

	/**
	 * How many purchasable/in-stock candidates get_sample_products() gathers
	 * (scan cap permitting) before picking the best one, so it can prefer a
	 * product with real SKU/tags/description data over whichever qualifying
	 * product merely happened to be newest. A single product page hooks the
	 * Product Page recommendation locations onto WooCommerce's meta/tabs
	 * hooks (see ProductPageRecommendations::locations()), and those regions
	 * render as visibly empty for a product with none of that data — so the
	 * preview picking such a product makes every location look broken even
	 * though nothing is actually wrong.
	 */
	const SAMPLE_PRODUCTS_POOL_TARGET = 20;

	/**
	 * Fetch a small pool of real, purchasable, in-stock simple products to
	 * stand in for recommendation content in preview mode. Pages through the
	 * catalog (newest first) collecting qualifying candidates, then returns
	 * the ones whose data best covers every Product Page anchor (SKU/tags for
	 * the meta line, description/short description for the tab regions) —
	 * falling back to plain newest-first order when nothing scores higher.
	 * Memoized per request.
	 *
	 * @param int $count How many products to fetch.
	 * @return \WC_Product[]
	 */
	public static function get_sample_products( $count = 12 ) {
		$count = max( 1, (int) $count );

		if ( isset( self::$sample_products_cache[ $count ] ) ) {
			return self::$sample_products_cache[ $count ];
		}

		$qualifying = array();

		if ( function_exists( 'wc_get_products' ) ) {
			$page        = 1;
			$scanned     = 0;
			$found_qty   = 0;
			$pool_target = max( $count, self::SAMPLE_PRODUCTS_POOL_TARGET );

			do {
				$found = wc_get_products(
					array(
						'limit'   => self::SAMPLE_PRODUCTS_BATCH_SIZE,
						'page'    => $page,
						'status'  => 'publish',
						'type'    => 'simple',
						'orderby' => 'date',
						'order'   => 'DESC',
					)
				);

				if ( empty( $found ) || ! is_array( $found ) ) {
					break;
				}

				$found_count = count( $found );

				foreach ( $found as $product ) {
					if ( $product instanceof \WC_Product && $product->is_purchasable() && $product->is_in_stock() ) {
						$qualifying[] = $product;
						++$found_qty;
					}
				}

				$scanned += $found_count;
				++$page;
			} while ( self::SAMPLE_PRODUCTS_BATCH_SIZE === $found_count && $scanned < self::SAMPLE_PRODUCTS_MAX_SCAN && $found_qty < $pool_target );
		}

		$products = self::sort_by_completeness( $qualifying );
		$products = array_slice( $products, 0, $count );

		self::$sample_products_cache[ $count ] = $products;
		return $products;
	}

	/**
	 * Sort candidate products by how completely they populate the data that
	 * WooCommerce's own single-product hooks render (SKU, tags, short
	 * description, description), highest first. Ties keep the incoming
	 * (newest-first) order — done via an explicit index tiebreaker since
	 * array_multisort's stability isn't guaranteed on PHP < 8.0.
	 *
	 * @param \WC_Product[] $products Candidates, newest-first.
	 * @return \WC_Product[]
	 */
	private static function sort_by_completeness( array $products ) {
		if ( count( $products ) < 2 ) {
			return $products;
		}

		$scores  = array();
		$indexes = array();
		foreach ( $products as $i => $product ) {
			$scores[]  = self::completeness_score( $product );
			$indexes[] = $i;
		}

		array_multisort( $scores, SORT_DESC, $indexes, SORT_ASC, $products );

		return $products;
	}

	/**
	 * How many of {SKU, tags, short description, description} a product has
	 * set, 0-4. Used to prefer a preview product that renders real content at
	 * every Product Page anchor over one that leaves some locations blank.
	 *
	 * @param \WC_Product $product Product.
	 * @return int
	 */
	private static function completeness_score( \WC_Product $product ) {
		$score = 0;

		if ( '' !== $product->get_sku() ) {
			++$score;
		}
		if ( ! empty( $product->get_tag_ids() ) ) {
			++$score;
		}
		if ( '' !== trim( wp_strip_all_tags( $product->get_short_description() ) ) ) {
			++$score;
		}
		if ( '' !== trim( wp_strip_all_tags( $product->get_description() ) ) ) {
			++$score;
		}

		return $score;
	}

	/**
	 * Sample product IDs — see get_sample_products().
	 *
	 * @param int $count How many product IDs to return.
	 * @return int[]
	 */
	public static function get_sample_product_ids( $count = 12 ) {
		return array_map(
			function ( $p ) {
				return $p->get_id();
			},
			self::get_sample_products( $count )
		);
	}

	/**
	 * Build a pool of in-memory placeholder products for preview-mode
	 * recommendation cards, so the builder's live iframe never surfaces real
	 * store products/prices/photos to the person configuring rules. See
	 * DummyProduct for the mock title/price/category/rating/image each one
	 * carries. Memoized per request.
	 *
	 * @param int $count How many placeholder products to build.
	 * @return DummyProduct[]
	 */
	public static function get_dummy_products( $count = 12 ) {
		$count = max( 1, (int) $count );

		if ( isset( self::$dummy_products_cache[ $count ] ) ) {
			return self::$dummy_products_cache[ $count ];
		}

		$products = array();
		for ( $i = 0; $i < $count; $i++ ) {
			$products[] = new DummyProduct( $i );
		}

		self::$dummy_products_cache[ $count ] = $products;
		return $products;
	}

	/**
	 * Create a minimal real order so the Thank You page has something to
	 * render when the store has no orders yet. Flagged via ORDER_META_FLAG
	 * so it's identifiable, but otherwise a normal completed WC_Order — the
	 * order-received template needs a real, saved order + key to validate.
	 *
	 * @return \WC_Order|null
	 */
	public static function create_sample_order() {
		if ( ! function_exists( 'wc_create_order' ) ) {
			return null;
		}

		$products = self::get_sample_products( 1 );
		if ( empty( $products ) ) {
			return null;
		}

		$order = wc_create_order( array( 'customer_id' => get_current_user_id() ) );
		if ( is_wp_error( $order ) ) {
			return null;
		}

		$order->add_product( $products[0], 1 );
		$order->update_meta_data( self::ORDER_META_FLAG, 1 );
		$order->calculate_totals();
		$order->set_status( 'completed' );
		$order->save();

		return $order;
	}

	/**
	 * Append a "Preview item" label to cart line items added by the preview
	 * cart-fill endpoint, so a missed cleanup never confuses the admin when
	 * they later look at their own real cart.
	 *
	 * @param string $name Existing cart item name markup.
	 * @param array  $cart_item Cart item data.
	 * @return string
	 */
	public static function maybe_label_cart_item( $name, $cart_item ) {
		if ( empty( $cart_item[ self::CART_ITEM_FLAG ] ) ) {
			return $name;
		}
		return $name;
	}
}
