<?php
/**
 * Dummy-content location renderer for the admin builder's live iframe.
 *
 * @package PRECO
 */

namespace PRECO\Includes\Frontend\Core;

use PRECO\Includes\Frontend\Core\Page\LocationRenderer;

defined( 'ABSPATH' ) || exit;

/**
 * Renders recommendation locations using the current user's unsaved builder
 * draft. When PreviewMode::use_real_products() is on (the default), each
 * list's actual filter rules are evaluated against the real store — same
 * engine and cache the live storefront uses — so the preview shows genuine
 * rule-matched products; a list with no real matches (or nothing configured
 * yet) falls back to PreviewMode's placeholder products, with a notice
 * banner in the "configured but zero matches" case so it reads as "no real
 * matches for this preview subject", not a broken widget. Setting
 * PRECO_PREVIEW_MODE to 'dummy' restores the old always-placeholder behavior.
 */
class PreviewRenderer {

	/**
	 * Shared production renderer.
	 *
	 * @var RecommendationsRenderer
	 */
	private $renderer;

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

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

	/**
	 * Constructor.
	 *
	 * @param RecommendationsRenderer $renderer Shared production renderer, reused for markup parity.
	 * @param RecommendationsEngine   $engine   Shared production rule-matching engine.
	 */
	public function __construct( RecommendationsRenderer $renderer, RecommendationsEngine $engine ) {
		$this->renderer = $renderer;
		$this->engine   = $engine;
		// Own instance rather than injected, mirroring LocationRenderer's
		// pattern — the cache is stateless infrastructure, not something
		// callers need to swap.
		$this->cache = new RecommendationsCache();
	}

	/**
	 * Render one location using the current draft's design settings, and
	 * either real rule-matched products or placeholder ones (see class
	 * docblock) depending on PreviewMode::use_real_products().
	 *
	 * @param string $page_key Page key.
	 * @param string $location_key Location key.
	 * @param array  $context Rule-evaluation context for this page/location —
	 *                        same shape each page's build_context() produces
	 *                        (product_id / exclude_ids / queried_term_* /
	 *                        order_id as applicable). Only used when real
	 *                        products are being queried; ignored otherwise.
	 * @return void
	 */
	public function render_location( $page_key, $location_key, $context = array() ) {
		$loc      = PreviewMode::get_draft_location( $page_key, $location_key );
		$settings = $loc['settings'];
		$lists    = $loc['recommendedLists'];

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

		// Preview renders every configured list regardless of its
		// active/inactive status — real storefront visits go through
		// LocationRenderer instead, which still honors `enabled`. Showing
		// both here is what lets the injected toolbar's toggle control
		// (see inject-controls.js) actually target disabled lists.
		$lists = array_values( array_filter( $lists, 'is_array' ) );

		// Nothing configured yet — emit an empty (zero-height, invisible)
		// anchor carrying the same data attributes real widgets get, so the
		// builder JS still has something to find and attach an "+ Add"
		// control to. No product content here — only configured lists show
		// product cards (see the loop below).
		if ( empty( $lists ) ) {
			printf(
				'<div class="preco-preview-anchor" data-page-source="%1$s" data-location-source="%2$s"></div>',
				esc_attr( $page_key ),
				esc_attr( $location_key )
			);
			return;
		}

		$use_real = PreviewMode::use_real_products();
		$subject  = $use_real ? $this->resolve_preview_subject( $page_key, $context ) : '';

		foreach ( $lists as $index => $list ) {
			$limit = isset( $list['limit'] ) && (int) $list['limit'] > 0 ? (int) $list['limit'] : 4;

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

			$wrapper_class = LocationRenderer::build_wrapper_class( $page_key, $location_key, $index );

			$rules_filtered   = $this->filter_enabled_items( isset( $list['rules'] ) && is_array( $list['rules'] ) ? $list['rules'] : array() );
			$sorting_filtered = $this->filter_enabled_items( isset( $list['sorting'] ) && is_array( $list['sorting'] ) ? $list['sorting'] : array() );
			$has_config       = ! empty( $rules_filtered ) || ! empty( $sorting_filtered );

			$html = '';

			if ( $use_real && $has_config && function_exists( 'wc_get_products' ) ) {
				$match_mode   = isset( $list['matchMode'] ) && 'any' === (string) $list['matchMode'] ? 'any' : 'all';
				$list_context = array_merge( $context, array( 'match_mode' => $match_mode ) );

				$product_ids = $this->query_real_ids( $page_key, $location_key, $index, $rules_filtered, $sorting_filtered, $list_context, $limit );

				if ( ! empty( $product_ids ) ) {
					$real_products = wc_get_products(
						array(
							'include' => $product_ids,
							'limit'   => count( $product_ids ),
							'orderby' => 'include',
							'status'  => 'publish',
						)
					);
					ob_start();
					$this->renderer->render_product_objects( $real_products, $wrapper_class, $list );
					$html = ob_get_clean();
				}
			}

			// True only when real matching was actually attempted (rules were
			// configured) and it came up empty — a brand-new list with no
			// rules yet falls straight through to here too, but silently
			// (nothing to explain: the admin just hasn't configured it yet).
			$used_fallback = $use_real && $has_config && '' === trim( $html );

			if ( '' === trim( $html ) ) {
				$dummy_products = PreviewMode::get_dummy_products( $limit );
				if ( empty( $dummy_products ) ) {
					continue;
				}
				ob_start();
				$this->renderer->render_product_objects( $dummy_products, $wrapper_class, $list );
				$html = ob_get_clean();
			}

			if ( '' === trim( $html ) ) {
				continue;
			}

			if ( $used_fallback ) {
				$filter_label = $this->resolve_filter_label( $rules_filtered, $match_mode );
				$reason       = $this->resolve_fallback_reason( $rules_filtered, $subject );
				$html         = $this->inject_fallback_notice( $html, $filter_label, $reason );
			}

			echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built entirely from esc_*()-processed renderer/template output plus our own esc_html()-processed notice markup.
		}
	}

	/**
	 * Resolve cached (or freshly queried) real product IDs for one list,
	 * mirroring LocationRenderer::render()'s cache-tier selection exactly so
	 * a preview render of an unchanged rule set is a transient cache hit
	 * (the same one a real shopper's page would hit) instead of paying full
	 * query cost on every debounced edit.
	 *
	 * @param string $page_key Page key.
	 * @param string $location_key Location key.
	 * @param int    $list_index List index within the location.
	 * @param array  $rules Enabled filter rules.
	 * @param array  $sorting Enabled sorting rules.
	 * @param array  $context Rule-evaluation context, including match_mode.
	 * @param int    $limit Requested product count.
	 * @return int[]
	 */
	private function query_real_ids( $page_key, $location_key, $list_index, $rules, $sorting, $context, $limit ) {
		if ( $this->cache->should_skip_cache( $sorting ) || $this->cache->rules_require_no_cache( $rules ) ) {
			$ids = $this->engine->get_product_ids( $rules, $sorting, $context, $limit );
			return is_array( $ids ) ? $ids : array();
		}

		$cache_key = $this->cache->make_key( $page_key, $location_key, $list_index, $rules, $sorting, $context, $limit );
		$ids       = $this->cache->get( $cache_key );

		if ( null === $ids ) {
			$ids = $this->engine->get_product_ids( $rules, $sorting, $context, $limit );
			$ids = is_array( $ids ) ? $ids : array();
			$this->cache->set( $cache_key, $ids, $this->cache->resolve_ttl( $rules ) );
		}

		return $ids;
	}

	/**
	 * A short human-readable label for the current preview subject, used in
	 * the fallback notice's copy (e.g. the product name on Product Page).
	 * Empty when the page type has no single natural subject (cart,
	 * checkout, shop) or it couldn't be resolved — callers fall back to
	 * generic copy in that case.
	 *
	 * @param string $page_key Page key.
	 * @param array  $context Rule-evaluation context.
	 * @return string
	 */
	private function resolve_preview_subject( $page_key, $context ) {
		if ( 'product-page' === $page_key && ! empty( $context['product_id'] ) && function_exists( 'wc_get_product' ) ) {
			$product = wc_get_product( absint( $context['product_id'] ) );
			if ( $product instanceof \WC_Product ) {
				return $product->get_name();
			}
		}
		return '';
	}

	/**
	 * Splice a fallback-notice banner in as the first child of the rendered
	 * wrapper div, so it lands inside the same bordered card as the product
	 * grid (and therefore inside the builder's injected dashed preview
	 * wrapper — see inject-controls.js, which moves whole
	 * [data-page-source][data-location-source] elements, not their
	 * siblings). Safe string surgery, not a template change: every
	 * attribute value in recommendations-grid.php's root <div> is
	 * esc_attr()'d, so it can never contain a raw '>' — the first '>' in the
	 * buffered output is always the end of that opening tag.
	 *
	 * @param string $html Buffered render_product_objects() output.
	 * @param string $filter_label Filter label(s) from resolve_filter_label(), or ''.
	 * @param string $reason Reason clause from resolve_fallback_reason(), or ''.
	 * @return string
	 */
	private function inject_fallback_notice( $html, $filter_label, $reason ) {
		$pos = strpos( $html, '>' );
		if ( false === $pos ) {
			return $html;
		}
		return substr( $html, 0, $pos + 1 ) . $this->build_fallback_notice_html( $filter_label, $reason ) . substr( $html, $pos + 1 );
	}

	/**
	 * Build the fallback-notice banner markup. Names the filter that was
	 * configured and, when the trigger type is well-understood, *why* it
	 * came up empty — e.g. "No products currently match the "Newest
	 * Products" filter because your store hasn't added any new products in
	 * the last 3 months." — so the admin understands the specific cause
	 * instead of just seeing an unexplained placeholder. Ends with "Try a
	 * different filter" (an action the admin genuinely can take right here)
	 * rather than ever suggesting they switch the preview subject, which
	 * this builder has no control for.
	 *
	 * @param string $filter_label Filter label(s), or '' if the list has no rules (sorting-only).
	 * @param string $reason Reason clause from resolve_fallback_reason() (e.g. "because …"), or ''.
	 * @return string
	 */
	private function build_fallback_notice_html( $filter_label, $reason ) {
		if ( '' !== $filter_label && '' !== $reason ) {
			$message = sprintf(
				/* translators: 1: filter name, e.g. "Newest Products". 2: reason clause, e.g. "because your store hasn't added any new products in the last 3 months". */
				__( 'No products currently match the "%1$s" filter %2$s. Try a different filter.', 'wow-ai-product-recommendations' ),
				$filter_label,
				$reason
			);
		} elseif ( '' !== $filter_label ) {
			$message = sprintf(
				/* translators: %s: filter name, e.g. "Cross-sell Products". */
				__( 'No products currently match the "%s" filter. Try a different filter.', 'wow-ai-product-recommendations' ),
				$filter_label
			);
		} else {
			$message = __( 'No products currently match this filter. Try a different filter.', 'wow-ai-product-recommendations' );
		}

		return sprintf(
			'<div class="preco-preview-fallback-notice"><svg class="preco-preview-fallback-notice__icon" viewBox="0 0 16 16" fill="none" aria-hidden="true"><circle cx="8" cy="8" r="7" fill="currentColor" opacity="0.15"/><path d="M8 7v4.5M8 4.75v.01" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg><span>%s</span></div>',
			esc_html( $message )
		);
	}

	/**
	 * A specific, data-aware explanation of why a list's rules matched zero
	 * products, when the answer is knowable — e.g. for a "Newest Products"
	 * filter windowed to the last 3 months, whether the store has actually
	 * added anything in that window. Only attempted when exactly one filter
	 * rule drives the (empty) result: with 0 or 2+ combined rules it's
	 * ambiguous which one is actually responsible, so callers fall back to
	 * the generic notice instead.
	 *
	 * @param array  $rules Enabled filter rules for the list.
	 * @param string $subject Preview subject label from resolve_preview_subject(), or ''.
	 * @return string Reason clause starting with "because"/"in", or '' if not knowable.
	 */
	private function resolve_fallback_reason( $rules, $subject ) {
		if ( 1 !== count( $rules ) || ! is_array( $rules[0] ) ) {
			return '';
		}
		$rule  = $rules[0];
		$type  = isset( $rule['triggerType'] ) ? (string) $rule['triggerType'] : '';
		$value = array_key_exists( 'value', $rule ) ? $rule['value'] : null;

		switch ( $type ) {
			case 'created':
				$timeframe = self::timeframe_phrase( $value );
				return '' !== $timeframe
					? sprintf(
						/* translators: %s: timeframe phrase, e.g. "in the last 3 months". */
						__( "because your store hasn't added any new products %s", 'wow-ai-product-recommendations' ),
						$timeframe
					)
					: '';

			case 'on_sale':
				return __( 'because no products in your store are currently on sale', 'wow-ai-product-recommendations' );

			case 'featured':
				return __( 'because no products in your store are marked as featured', 'wow-ai-product-recommendations' );

			case 'stock_status':
				$status = self::stock_status_phrase( $value );
				return '' !== $status
					? sprintf(
						/* translators: %s: stock status label, e.g. "Out of Stock". */
						__( 'because no products currently have "%s" status', 'wow-ai-product-recommendations' ),
						$status
					)
					: '';

			case 'category':
			case 'tag':
				$names = self::term_names_phrase( $value, 'category' === $type ? 'product_cat' : 'product_tag' );
				return sprintf(
					/* translators: %s: category/tag names, or "the selected value(s)" when they can't be resolved. */
					__( 'because no products are in %s', 'wow-ai-product-recommendations' ),
					'' !== $names ? $names : __( 'the selected value(s)', 'wow-ai-product-recommendations' )
				);

			case 'brand':
				return __( 'because no products are in the selected brand(s)', 'wow-ai-product-recommendations' );

			case 'attribute':
				return __( 'because no products match the selected attribute value(s)', 'wow-ai-product-recommendations' );

			case 'product_price':
			case 'relative_price':
				return __( 'because no products fall within the configured price range', 'wow-ai-product-recommendations' );

			case 'specific_products':
				return __( 'because the selected product(s) may be unpublished, out of stock, or unavailable', 'wow-ai-product-recommendations' );

			case 'upsells':
			case 'cross_sells':
				if ( '' === $subject ) {
					return '';
				}
				return sprintf(
					/* translators: 1: the product currently being previewed. 2: relationship noun, e.g. "cross-sell products". */
					__( 'because "%1$s" has no %2$s set in WooCommerce', 'wow-ai-product-recommendations' ),
					$subject,
					'upsells' === $type
						? __( 'upsell products', 'wow-ai-product-recommendations' )
						: __( 'cross-sell products', 'wow-ai-product-recommendations' )
				);

			case 'related':
				return '' !== $subject
					? sprintf(
						/* translators: %s: the product currently being previewed. */
						__( "because WooCommerce couldn't find other products sharing \"%s\"'s categories or tags", 'wow-ai-product-recommendations' ),
						$subject
					)
					: '';

			case 'bought_together':
				return '' !== $subject
					? sprintf(
						/* translators: %s: the product currently being previewed. */
						__( "because there isn't enough order history yet showing products bought together with \"%s\"", 'wow-ai-product-recommendations' ),
						$subject
					)
					: __( "because there isn't enough order history yet for this", 'wow-ai-product-recommendations' );

			case 'best_selling':
			case 'high_revenue':
			case 'low_selling':
			case 'high_conversion':
			case 'high_click_through':
				return __( "because your store doesn't have enough sales data for this yet", 'wow-ai-product-recommendations' );

			case 'recently_viewed':
			case 'products_viewed':
			case 'frequently_viewed':
			case 'added_to_cart':
			case 'removed_from_cart':
			case 'previously_purchased':
			case 'customer_interest_categories':
			case 'customer_interest_tags':
			case 'customer_interest_attributes':
				return __( "because this depends on a real shopper's browsing activity, which isn't available while previewing", 'wow-ai-product-recommendations' );

			default:
				return '';
		}
	}

	/**
	 * Natural-language phrase for a "created" (Newest Products) rule's
	 * timeframe value, mirroring TIMEFRAME_OPTIONS in options.js.
	 *
	 * @param mixed $value Rule value — expected to be one of DateRuleHelper::add_created_rule()'s keys.
	 * @return string
	 */
	private static function timeframe_phrase( $value ) {
		$map = array(
			'last_24h' => __( 'in the last 24 hours', 'wow-ai-product-recommendations' ),
			'last_7d'  => __( 'in the last 7 days', 'wow-ai-product-recommendations' ),
			'last_30d' => __( 'in the last 30 days', 'wow-ai-product-recommendations' ),
			'last_3m'  => __( 'in the last 3 months', 'wow-ai-product-recommendations' ),
		);
		$key = is_string( $value ) ? $value : '';
		return isset( $map[ $key ] ) ? $map[ $key ] : '';
	}

	/**
	 * Display label for a "stock_status" rule's value, mirroring
	 * MetaRuleHelper::add_stock_status_rule()'s status_map keys.
	 *
	 * @param mixed $value Rule value.
	 * @return string
	 */
	private static function stock_status_phrase( $value ) {
		$map = array(
			'in_stock'     => __( 'In Stock', 'wow-ai-product-recommendations' ),
			'out_of_stock' => __( 'Out of Stock', 'wow-ai-product-recommendations' ),
			'backorder'    => __( 'On Backorder', 'wow-ai-product-recommendations' ),
		);
		$key = is_string( $value ) ? $value : '';
		return isset( $map[ $key ] ) ? $map[ $key ] : '';
	}

	/**
	 * Resolve up to 3 quoted term names for a category/tag rule's value
	 * (stored as slugs — see TaxRuleHelper::coerce_terms()), for a more
	 * specific reason than "the selected value(s)". Returns '' if nothing
	 * resolves (taxonomy missing, stale slugs, etc.) so the caller can fall
	 * back to generic copy.
	 *
	 * @param mixed  $value Rule value (comma string or array of slugs).
	 * @param string $taxonomy Taxonomy to resolve slugs against.
	 * @return string
	 */
	private static function term_names_phrase( $value, $taxonomy ) {
		if ( ! function_exists( 'get_term_by' ) ) {
			return '';
		}
		$slugs = is_string( $value ) && '' !== trim( $value )
			? array_values( array_filter( array_map( 'trim', explode( ',', $value ) ) ) )
			: ( is_array( $value ) ? array_values( $value ) : array() );
		if ( empty( $slugs ) ) {
			return '';
		}

		$names = array();
		foreach ( array_slice( $slugs, 0, 3 ) as $slug ) {
			$term = get_term_by( 'slug', sanitize_title( (string) $slug ), $taxonomy );
			if ( $term && ! is_wp_error( $term ) ) {
				$names[] = '"' . $term->name . '"';
			}
		}
		if ( empty( $names ) ) {
			return '';
		}

		$suffix = count( $slugs ) > 3 ? ' ' . __( 'and others', 'wow-ai-product-recommendations' ) : '';
		return implode( ', ', $names ) . $suffix;
	}

	/**
	 * Human-readable label(s) for the filter rule(s) that were configured
	 * for a list, joined by "+" (AND) or "or" (OR) per the list's match
	 * mode. Empty when the list has no filter rules at all (sorting-only
	 * lists don't have a "filter" to name).
	 *
	 * @param array  $rules Enabled filter rules.
	 * @param string $match_mode 'all' or 'any'.
	 * @return string
	 */
	private function resolve_filter_label( $rules, $match_mode ) {
		$labels = array();
		foreach ( $rules as $rule ) {
			if ( ! is_array( $rule ) ) {
				continue;
			}
			$type = isset( $rule['triggerType'] ) ? (string) $rule['triggerType'] : '';
			if ( '' === $type ) {
				continue;
			}
			$label = self::trigger_type_label( $type );
			if ( ! in_array( $label, $labels, true ) ) {
				$labels[] = $label;
			}
		}
		if ( empty( $labels ) ) {
			return '';
		}
		$joiner = 'any' === $match_mode ? ' ' . __( 'or', 'wow-ai-product-recommendations' ) . ' ' : ' + ';
		return implode( $joiner, $labels );
	}

	/**
	 * Trigger-type value → display label, mirroring TRIGGER_OPTIONS in
	 * src/builder/.../rules-builder/utils/rules/options.js — keep the two in
	 * sync if a label changes there. Falls back to a title-cased version of
	 * the raw type for anything not in this map (e.g. a type added via the
	 * preco_recommendations_apply_rule filter) rather than showing nothing.
	 *
	 * @param string $type Trigger type value.
	 * @return string
	 */
	private static function trigger_type_label( $type ) {
		static $labels = null;
		if ( null === $labels ) {
			$labels = array(
				'specific_products'            => __( 'Specific Products', 'wow-ai-product-recommendations' ),
				'category'                     => __( 'Product Categories', 'wow-ai-product-recommendations' ),
				'attribute'                    => __( 'Product Attributes', 'wow-ai-product-recommendations' ),
				'tag'                          => __( 'Product Tags', 'wow-ai-product-recommendations' ),
				'brand'                        => __( 'Brand', 'wow-ai-product-recommendations' ),
				'current_category'             => __( "Current Product's Category", 'wow-ai-product-recommendations' ),
				'current_tag'                  => __( "Current Product's Tag", 'wow-ai-product-recommendations' ),
				'current_attribute'            => __( "Current Product's Attribute", 'wow-ai-product-recommendations' ),
				'product_price'                => __( 'Product Price', 'wow-ai-product-recommendations' ),
				'stock_status'                 => __( 'Stock Status', 'wow-ai-product-recommendations' ),
				'relative_price'               => __( 'Relative Price', 'wow-ai-product-recommendations' ),
				'created'                      => __( 'Newest Products', 'wow-ai-product-recommendations' ),
				'best_selling'                 => __( 'Best-Selling Products', 'wow-ai-product-recommendations' ),
				'high_revenue'                 => __( 'High-Revenue Products', 'wow-ai-product-recommendations' ),
				'low_selling'                  => __( 'Low-Selling Products', 'wow-ai-product-recommendations' ),
				'high_conversion'              => __( 'High Conversion Products', 'wow-ai-product-recommendations' ),
				'on_sale'                      => __( 'On Sale Products', 'wow-ai-product-recommendations' ),
				'featured'                     => __( 'Featured Products', 'wow-ai-product-recommendations' ),
				'high_click_through'           => __( 'High Click-Through Products', 'wow-ai-product-recommendations' ),
				'products_viewed'              => __( 'Products Viewed', 'wow-ai-product-recommendations' ),
				'recently_viewed'              => __( 'Recently Viewed', 'wow-ai-product-recommendations' ),
				'frequently_viewed'            => __( 'Frequently Viewed', 'wow-ai-product-recommendations' ),
				'added_to_cart'                => __( 'Added to Cart', 'wow-ai-product-recommendations' ),
				'removed_from_cart'            => __( 'Removed from Cart', 'wow-ai-product-recommendations' ),
				'previously_purchased'         => __( 'Previously Purchased', 'wow-ai-product-recommendations' ),
				'customer_interest_categories' => __( 'Customer Interest Categories', 'wow-ai-product-recommendations' ),
				'customer_interest_tags'       => __( 'Customer Interest Tags', 'wow-ai-product-recommendations' ),
				'customer_interest_attributes' => __( 'Customer Interest Attributes', 'wow-ai-product-recommendations' ),
				'related'                      => __( 'Related Products', 'wow-ai-product-recommendations' ),
				'upsells'                      => __( 'Upsell Products', 'wow-ai-product-recommendations' ),
				'cross_sells'                  => __( 'Cross-sell Products', 'wow-ai-product-recommendations' ),
				'bought_together'              => __( 'Complementary / Bought Together Products', 'wow-ai-product-recommendations' ),
			);
		}
		return isset( $labels[ $type ] ) ? $labels[ $type ] : ucwords( str_replace( '_', ' ', $type ) );
	}

	/**
	 * Filter out disabled rule/sorting rows. Small private duplicate of
	 * LocationRenderer's identical helper (also duplicated in
	 * OrderBumpRecommendations/PopupUpsellRecommendations) rather than
	 * widening that class's API for one more caller.
	 *
	 * @param array $items Rows.
	 * @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;
	}
}
