<?php
/**
 * Storzen Module — Sticky Add to Cart
 *
 * Renders a floating product bar (thumbnail + title + price + ATC + Buy Now)
 * that appears as the customer scrolls past the configured offset on single
 * product pages. Massive conversion lift on long product pages.
 *
 * Works with:
 *   • Simple products (full AJAX add-to-cart + Buy Now)
 *   • Variable products (button scrolls back to the variation form)
 *   • Out-of-stock / unpurchasable products are skipped entirely
 *
 * Compatibility:
 *   • Classic + Block themes
 *   • HPOS (no order touches — only wc_get_product)
 *   • Mobile + desktop with independent device toggles
 *
 * @package storzen
 * @since   1.1.0
 */

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

if ( ! class_exists( 'Storzen_Module_Sticky_Atc' ) ) {

	class Storzen_Module_Sticky_Atc {

		/** AJAX action — Buy Now (add to cart + return checkout URL) */
		const AJAX_BUY_NOW = 'storzen_buy_now';

		/** @var array Resolved settings cache (per request) */
		private $settings = null;

		public function __construct() {
			if ( ! function_exists( 'WC' ) ) {
				return;
			}

			add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
			add_action( 'wp_footer',          array( $this, 'render_bar' ), 20 );

			// Buy Now endpoint — adds to cart + responds with checkout redirect URL.
			add_action( 'wp_ajax_'        . self::AJAX_BUY_NOW, array( $this, 'ajax_buy_now' ) );
			add_action( 'wp_ajax_nopriv_' . self::AJAX_BUY_NOW, array( $this, 'ajax_buy_now' ) );

			// Track recently viewed products into WC's standard cookie. WC only
			// populates this cookie via the recently-viewed shortcode side-effect,
			// so we do it here for reliability when the Pro "Recently Viewed"
			// feature is on. Cookie name: 'woocommerce_recently_viewed'.
			add_action( 'template_redirect', array( $this, 'track_recently_viewed' ) );
		}

		// ─────────────────────────────────────────────────────────────────
		// Settings helper
		// ─────────────────────────────────────────────────────────────────

		/**
		 * Resolve module settings once per request with safe defaults.
		 * Pro settings (`pro_*`) collapse to safe defaults when STORZEN_IS_PRO
		 * is not 'yes'.
		 *
		 * @return array
		 */
		private function get_settings() {
			if ( null !== $this->settings ) {
				return $this->settings;
			}

			$positions  = array( 'bottom', 'top' );
			$animations = array( 'slide', 'fade' );

			$position = (string) storzen_module_setting( 'sticky_atc', 'position' );
			if ( ! in_array( $position, $positions, true ) ) {
				$position = 'bottom';
			}

			$animation = (string) storzen_module_setting( 'sticky_atc', 'animation' );
			if ( ! in_array( $animation, $animations, true ) ) {
				$animation = 'slide';
			}

			$button_text = (string) storzen_module_setting( 'sticky_atc', 'button_text' );
			if ( '' === $button_text ) {
				$button_text = __( 'Add to Cart', 'storzen' );
			}

			$buy_now_text = (string) storzen_module_setting( 'sticky_atc', 'buy_now_text' );
			if ( '' === $buy_now_text ) {
				$buy_now_text = __( 'Buy Now', 'storzen' );
			}

			$is_pro = defined( 'STORZEN_IS_PRO' ) && 'yes' === STORZEN_IS_PRO;

			$this->settings = array(
				// Free
				'position'        => $position,
				'animation'       => $animation,
				'trigger_offset'  => max( 100, min( 3000, (int) storzen_module_setting( 'sticky_atc', 'trigger_offset' ) ) ),
				'button_text'     => $button_text,
				'buy_now_text'    => $buy_now_text,
				'show_buy_now'    => (bool) storzen_module_setting( 'sticky_atc', 'show_buy_now' ),
				'show_thumbnail'  => (bool) storzen_module_setting( 'sticky_atc', 'show_thumbnail' ),
				'show_title'      => (bool) storzen_module_setting( 'sticky_atc', 'show_title' ),
				'show_price'      => (bool) storzen_module_setting( 'sticky_atc', 'show_price' ),
				'show_quantity'   => (bool) storzen_module_setting( 'sticky_atc', 'show_quantity' ),
				'show_on_mobile'  => (bool) storzen_module_setting( 'sticky_atc', 'show_on_mobile' ),
				'show_on_desktop' => (bool) storzen_module_setting( 'sticky_atc', 'show_on_desktop' ),
				'shadow'          => (bool) storzen_module_setting( 'sticky_atc', 'shadow' ),
				'border_top'      => (bool) storzen_module_setting( 'sticky_atc', 'border_top' ),

				// Pro (gated by STORZEN_IS_PRO)
				'pro_show_savings'     => $is_pro && (bool) storzen_module_setting( 'sticky_atc', 'pro_show_savings' ),
				'pro_recently_viewed'  => $is_pro && (bool) storzen_module_setting( 'sticky_atc', 'pro_recently_viewed' ),
				'pro_countdown_in_bar' => $is_pro && (bool) storzen_module_setting( 'sticky_atc', 'pro_countdown_in_bar' ),
				'pro_progress_to_goal' => $is_pro && (bool) storzen_module_setting( 'sticky_atc', 'pro_progress_to_goal' ),
				'pro_ab_test_label'    => $is_pro && (bool) storzen_module_setting( 'sticky_atc', 'pro_ab_test_label' ),
				'pro_ab_test_alt_label'=> $is_pro ? (string) storzen_module_setting( 'sticky_atc', 'pro_ab_test_alt_label' ) : '',
				'pro_custom_css'       => $is_pro ? (string) storzen_module_setting( 'sticky_atc', 'pro_custom_css' ) : '',
				'is_pro_active'        => $is_pro,
			);

			return $this->settings;
		}

		// ─────────────────────────────────────────────────────────────────
		// Asset enqueue (single product pages only)
		// ─────────────────────────────────────────────────────────────────

		public function enqueue_assets() {
			if ( is_admin() ) {
				return;
			}
			if ( ! function_exists( 'is_product' ) || ! is_product() ) {
				return;
			}
			if ( class_exists( 'Elementor\Storzen_Archive_Grid' ) && false === \Elementor\Storzen_Archive_Grid::is_page_module_enabled( 'sticky_atc' ) ) {
				return;
			}

			$settings = $this->get_settings();

			wp_enqueue_style(
				'storzen-sticky-atc',
				STORZEN_ROOT_URL . 'assets/css/modules/sticky-atc.css',
				array(),
				STORZEN_VERSION
			);

			// Pro: user-supplied custom CSS. Strip tags as a basic guard; CSS doesn't
			// need angle brackets so this is safe and avoids style breakouts.
			if ( ! empty( $settings['pro_custom_css'] ) ) {
				wp_add_inline_style(
					'storzen-sticky-atc',
					"/* Sticky ATC user custom CSS */\n" . wp_strip_all_tags( $settings['pro_custom_css'] )
				);
			}

			wp_enqueue_script(
				'storzen-sticky-atc',
				STORZEN_ROOT_URL . 'assets/js/modules/sticky-atc.js',
				array( 'jquery' ),
				STORZEN_VERSION,
				true
			);

			// Current cart total used as the starting point of the free-shipping
			// progress bar. Recomputed live on the client when 'added_to_cart'
			// fires; this is just the initial render value.
			$cart_total = ( WC()->cart && method_exists( WC()->cart, 'get_displayed_subtotal' ) )
				? (float) WC()->cart->get_displayed_subtotal()
				: 0;

			wp_localize_script(
				'storzen-sticky-atc',
				'szStickyAtc',
				array(
					'ajaxUrl'      => admin_url( 'admin-ajax.php' ),
					'checkoutUrl'  => function_exists( 'wc_get_checkout_url' ) ? wc_get_checkout_url() : '',
					'cartUrl'      => function_exists( 'wc_get_cart_url' ) ? wc_get_cart_url() : '',
					'cartTotal'    => $cart_total,
					'currencySym'  => function_exists( 'get_woocommerce_currency_symbol' ) ? get_woocommerce_currency_symbol() : '$',
					'abTestEnabled'=> (bool) $settings['pro_ab_test_label'],
					'i18n'         => array(
						'adding'      => __( 'Adding…', 'storzen' ),
						'added'       => __( 'Added!', 'storzen' ),
						'error'       => __( 'Could not add to cart. Please try again.', 'storzen' ),
						'goingTo'     => __( 'Going to checkout…', 'storzen' ),
						'shipReached' => __( '🎉 You unlocked free shipping!', 'storzen' ),
						/* translators: %s: remaining amount needed for free shipping (substituted in JS) */
						'shipMore'    => __( 'Add %s more for free shipping', 'storzen' ),
						'days'        => __( 'd', 'storzen' ),
						'hours'       => __( 'h', 'storzen' ),
						'mins'        => __( 'm', 'storzen' ),
						'secs'        => __( 's', 'storzen' ),
					),
				)
			);
		}

		// ─────────────────────────────────────────────────────────────────
		// Sticky bar render
		// ─────────────────────────────────────────────────────────────────

		/**
		 * Render the sticky ATC bar in the page footer.
		 */
		public function render_bar() {
			if ( is_admin() ) {
				return;
			}
			if ( ! function_exists( 'is_product' ) || ! is_product() ) {
				return;
			}
			if ( class_exists( 'Elementor\Storzen_Archive_Grid' ) && false === \Elementor\Storzen_Archive_Grid::is_page_module_enabled( 'sticky_atc' ) ) {
				return;
			}

			global $product; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- WC global.
			if ( ! $product instanceof WC_Product ) {
				$product = wc_get_product( get_the_ID() ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- WC global.
			}
			if ( ! $product instanceof WC_Product || ! $product->is_purchasable() || ! $product->is_in_stock() ) {
				return;
			}

			$settings = $this->get_settings();

			// If both device toggles are off there's nothing to show.
			if ( ! $settings['show_on_mobile'] && ! $settings['show_on_desktop'] ) {
				return;
			}

			$classes = array(
				'sz-sac',
				'sz-sac--' . sanitize_html_class( $settings['position'] ),
				'sz-sac--anim-' . sanitize_html_class( $settings['animation'] ),
			);
			if ( ! $settings['show_on_mobile'] )  { $classes[] = 'sz-sac--hide-mobile'; }
			if ( ! $settings['show_on_desktop'] ) { $classes[] = 'sz-sac--hide-desktop'; }
			if ( $settings['shadow'] )            { $classes[] = 'sz-sac--shadow'; }
			if ( $settings['border_top'] )        { $classes[] = 'sz-sac--border'; }

			$is_variable = $product->is_type( 'variable' );
			$nonce       = wp_create_nonce( 'storzen_sac_' . $product->get_id() );
			$max_qty     = $product->get_max_purchase_quantity();

			// ── Pro pre-computations (each gated by its own flag) ─────────
			$savings        = $settings['pro_show_savings']     ? $this->compute_savings( $product )            : null;
			$sale_end_ts    = $settings['pro_countdown_in_bar'] ? $this->get_sale_end_timestamp( $product )     : 0;
			$ship_threshold = $settings['pro_progress_to_goal'] ? $this->get_free_shipping_threshold()          : 0;
			$recently       = $settings['pro_recently_viewed']  ? $this->get_recently_viewed_products( $product->get_id() ) : array();

			// Bar background class gets a tighter top-padding when the recently-
			// viewed strip is present, so the bar doesn't look like two pieces.
			if ( ! empty( $recently ) ) {
				$classes[] = 'sz-sac--has-recent';
			}
			if ( $sale_end_ts > time() ) {
				$classes[] = 'sz-sac--has-countdown';
			}
			?>
			<div
				class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>"
				data-product-id="<?php echo esc_attr( (string) $product->get_id() ); ?>"
				data-product-type="<?php echo esc_attr( $product->get_type() ); ?>"
				data-trigger-offset="<?php echo esc_attr( (string) $settings['trigger_offset'] ); ?>"
				data-nonce="<?php echo esc_attr( $nonce ); ?>"
				<?php if ( $sale_end_ts > time() ) : ?>
					data-sale-end="<?php echo esc_attr( (string) $sale_end_ts ); ?>"
				<?php endif; ?>
				<?php if ( $ship_threshold > 0 ) : ?>
					data-ship-threshold="<?php echo esc_attr( (string) $ship_threshold ); ?>"
				<?php endif; ?>
				<?php if ( $settings['pro_ab_test_label'] && '' !== $settings['pro_ab_test_alt_label'] ) : ?>
					data-ab-alt-label="<?php echo esc_attr( $settings['pro_ab_test_alt_label'] ); ?>"
				<?php endif; ?>
				role="region"
				aria-label="<?php esc_attr_e( 'Sticky add to cart bar', 'storzen' ); ?>"
				aria-hidden="true"
			>
				<?php
				// Recently Viewed strip — ABOVE the main inner row.
				if ( ! empty( $recently ) ) {
					$this->render_recently_viewed_strip( $recently );
				}
				?>
				<div class="sz-sac__inner">

					<?php if ( $settings['show_thumbnail'] && $product->get_image_id() ) : ?>
						<div class="sz-sac__thumb">
							<?php
							echo wp_get_attachment_image(
								$product->get_image_id(),
								'thumbnail',
								false,
								array( 'alt' => esc_attr( $product->get_name() ) )
							);
							?>
						</div>
					<?php endif; ?>

					<div class="sz-sac__info">
						<?php if ( $settings['show_title'] ) : ?>
							<div class="sz-sac__title">
								<?php echo esc_html( wp_trim_words( $product->get_name(), 8 ) ); ?>
							</div>
						<?php endif; ?>

						<?php if ( $settings['show_price'] ) : ?>
							<div class="sz-sac__price">
								<?php echo wp_kses_post( $product->get_price_html() ); ?>
								<?php if ( $savings ) : ?>
									<span class="sz-sac__savings" aria-label="<?php
										/* translators: 1: amount saved, 2: percent off */
										printf( esc_attr__( 'Save %1$s, %2$d%% off', 'storzen' ), esc_attr( wp_strip_all_tags( wc_price( $savings['amount'] ) ) ), (int) $savings['percent'] );
									?>">
										<?php
										/* translators: %s: amount saved */
										printf( esc_html__( 'Save %s', 'storzen' ), wp_kses_post( wc_price( $savings['amount'] ) ) );
										?>
										<span class="sz-sac__savings-pct"><?php echo (int) $savings['percent']; ?>%</span>
									</span>
								<?php endif; ?>
							</div>
						<?php endif; ?>

						<?php if ( $sale_end_ts > time() ) : ?>
							<div class="sz-sac__countdown" data-end="<?php echo esc_attr( (string) $sale_end_ts ); ?>" aria-live="polite">
								<span class="sz-sac__countdown-label"><?php esc_html_e( 'Ends in', 'storzen' ); ?></span>
								<span class="sz-sac__countdown-time">--:--:--</span>
							</div>
						<?php endif; ?>
					</div>

					<div class="sz-sac__actions">

						<?php if ( $settings['show_quantity'] && ! $is_variable ) : ?>
							<div class="sz-sac__qty">
								<button type="button" class="sz-sac__qty-btn" data-action="dec" aria-label="<?php esc_attr_e( 'Decrease quantity', 'storzen' ); ?>">&minus;</button>
								<input
									type="number"
									class="sz-sac__qty-input"
									value="1"
									min="1"
									<?php echo $max_qty > 0 ? 'max="' . esc_attr( (string) $max_qty ) . '"' : ''; ?>
									aria-label="<?php esc_attr_e( 'Quantity', 'storzen' ); ?>"
								>
								<button type="button" class="sz-sac__qty-btn" data-action="inc" aria-label="<?php esc_attr_e( 'Increase quantity', 'storzen' ); ?>">+</button>
							</div>
						<?php endif; ?>

						<?php if ( $is_variable ) : ?>
							<a
								href="#"
								class="sz-sac__btn sz-sac__btn--scroll"
								data-target=".variations_form"
							>
								<span class="sz-sac__btn-label"><?php esc_html_e( 'Select Options', 'storzen' ); ?></span>
							</a>
						<?php else : ?>
							<button
								type="button"
								class="sz-sac__btn sz-sac__btn--atc"
								data-action="add-to-cart"
							>
								<span class="sz-sac__btn-label"><?php echo esc_html( $settings['button_text'] ); ?></span>
							</button>

							<?php if ( $settings['show_buy_now'] ) : ?>
								<button
									type="button"
									class="sz-sac__btn sz-sac__btn--buy"
									data-action="buy-now"
								>
									<span class="sz-sac__btn-label"><?php echo esc_html( $settings['buy_now_text'] ); ?></span>
								</button>
							<?php endif; ?>
						<?php endif; ?>

					</div>
				</div>

				<?php if ( $ship_threshold > 0 ) : ?>
					<div class="sz-sac__ship-progress"
						data-threshold="<?php echo esc_attr( (string) $ship_threshold ); ?>"
						aria-live="polite"
					>
						<div class="sz-sac__ship-message">
							<span class="sz-sac__ship-text" data-initial>
								<?php
								/* translators: %s: formatted price needed for free shipping */
								printf( esc_html__( 'Add %s more for free shipping', 'storzen' ), wp_kses_post( wc_price( $ship_threshold ) ) );
								?>
							</span>
						</div>
						<div class="sz-sac__ship-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
							<div class="sz-sac__ship-fill" style="width:0%"></div>
						</div>
					</div>
				<?php endif; ?>
			</div>
			<?php
		}

		// ─────────────────────────────────────────────────────────────────
		// Pro helpers — recently viewed, savings, sale end date, free-ship goal
		// ─────────────────────────────────────────────────────────────────

		/**
		 * Track the current product into WooCommerce's standard
		 * `woocommerce_recently_viewed` cookie. Capped at 15 entries.
		 * No-op if not viewing a single product.
		 */
		public function track_recently_viewed() {
			if ( ! function_exists( 'is_product' ) || ! is_product() ) {
				return;
			}
			// Only track when the feature is on — avoids setting cookies needlessly.
			$s = $this->get_settings();
			if ( empty( $s['pro_recently_viewed'] ) ) {
				return;
			}

			$product_id = (int) get_the_ID();
			if ( ! $product_id ) {
				return;
			}

			$raw  = isset( $_COOKIE['woocommerce_recently_viewed'] )
				? sanitize_text_field( wp_unslash( $_COOKIE['woocommerce_recently_viewed'] ) )
				: '';
			$ids  = array_filter( array_map( 'absint', explode( '|', $raw ) ) );

			// Move current product to the front, dedupe, cap at 15.
			$ids = array_diff( $ids, array( $product_id ) );
			array_unshift( $ids, $product_id );
			$ids = array_slice( $ids, 0, 15 );

			// Match WC's own cookie semantics (no httpOnly so JS can also read it).
			wc_setcookie( 'woocommerce_recently_viewed', implode( '|', $ids ) );
		}

		/**
		 * Build the list of products to show in the "Recently Viewed" strip,
		 * excluding the current product. Returns an array of WC_Product.
		 *
		 * @param int $exclude_id Current product ID to skip.
		 * @param int $limit
		 * @return WC_Product[]
		 */
		private function get_recently_viewed_products( $exclude_id, $limit = 8 ) {
			$raw = isset( $_COOKIE['woocommerce_recently_viewed'] )
				? sanitize_text_field( wp_unslash( $_COOKIE['woocommerce_recently_viewed'] ) )
				: '';
			$ids = array_filter( array_map( 'absint', explode( '|', $raw ) ) );
			$ids = array_diff( $ids, array( $exclude_id ) );

			if ( empty( $ids ) ) {
				return array();
			}

			$products = array();
			foreach ( $ids as $id ) {
				if ( count( $products ) >= $limit ) {
					break;
				}
				$p = wc_get_product( $id );
				if ( $p instanceof WC_Product && $p->is_visible() ) {
					$products[] = $p;
				}
			}
			return $products;
		}

		/**
		 * Compute on-sale savings for a product. Returns [amount, percent] or null
		 * if not on sale / can't compare meaningfully.
		 *
		 * @param WC_Product $product
		 * @return array|null  array{'amount': float, 'percent': int}
		 */
		private function compute_savings( $product ) {
			if ( ! $product->is_on_sale() ) {
				return null;
			}

			$regular = (float) $product->get_regular_price();
			$sale    = (float) $product->get_sale_price();

			// For variable products, use the lowest variation sale/regular.
			if ( $product->is_type( 'variable' ) ) {
				$variation_ids = $product->get_visible_children();
				if ( empty( $variation_ids ) ) {
					return null;
				}
				$reg_min  = INF;
				$sale_min = INF;
				foreach ( $variation_ids as $vid ) {
					$v = wc_get_product( $vid );
					if ( ! $v ) {
						continue;
					}
					$vr = (float) $v->get_regular_price();
					$vs = (float) $v->get_sale_price();
					if ( $vr > 0 && $vr < $reg_min )    { $reg_min  = $vr; }
					if ( $vs > 0 && $vs < $sale_min )   { $sale_min = $vs; }
				}
				$regular = ( $reg_min  !== INF ) ? $reg_min  : 0;
				$sale    = ( $sale_min !== INF ) ? $sale_min : 0;
			}

			if ( $regular <= 0 || $sale <= 0 || $sale >= $regular ) {
				return null;
			}

			$amount  = $regular - $sale;
			$percent = (int) round( ( $amount / $regular ) * 100 );

			return array(
				'amount'  => $amount,
				'percent' => $percent,
			);
		}

		/**
		 * Read the product's sale end date as a UTC timestamp, or 0 if none set.
		 *
		 * @param WC_Product $product
		 * @return int Unix timestamp (UTC), or 0.
		 */
		private function get_sale_end_timestamp( $product ) {
			$end = $product->get_date_on_sale_to();
			if ( $end ) {
				return (int) $end->getTimestamp();
			}
			// Variable: use the earliest variation sale end so the bar shows the
			// most urgent timer.
			if ( $product->is_type( 'variable' ) ) {
				$earliest = 0;
				foreach ( $product->get_visible_children() as $vid ) {
					$v = wc_get_product( $vid );
					if ( ! $v ) {
						continue;
					}
					$v_end = $v->get_date_on_sale_to();
					if ( $v_end ) {
						$ts = (int) $v_end->getTimestamp();
						if ( $ts > time() && ( 0 === $earliest || $ts < $earliest ) ) {
							$earliest = $ts;
						}
					}
				}
				return $earliest;
			}
			return 0;
		}

		/**
		 * Resolve the free-shipping threshold for the current customer.
		 *
		 * Prefers the Shipping Bar module's threshold so the two modules agree
		 * on the goal. Falls back to scanning WC shipping zones for a free-
		 * shipping method with a min_amount requirement.
		 *
		 * @return float 0 if no threshold could be determined.
		 */
		private function get_free_shipping_threshold() {
			// 1) Reuse Shipping Bar module's threshold when available.
			if ( function_exists( 'storzen_module_setting' ) ) {
				$bar_threshold = (float) storzen_module_setting( 'shipping_bar', 'threshold' );
				if ( $bar_threshold > 0 ) {
					return $bar_threshold;
				}
			}

			// 2) Walk WC shipping zones (default zone + matched zones) looking
			// for a free_shipping method with min_amount > 0.
			if ( ! class_exists( 'WC_Shipping_Zones' ) ) {
				return 0;
			}

			$candidates = array();
			foreach ( WC_Shipping_Zones::get_zones() as $zone ) {
				if ( empty( $zone['shipping_methods'] ) ) {
					continue;
				}
				foreach ( $zone['shipping_methods'] as $method ) {
					if ( 'free_shipping' === $method->id && 'yes' === $method->enabled ) {
						$min = isset( $method->min_amount ) ? (float) $method->min_amount : 0;
						if ( $min > 0 ) {
							$candidates[] = $min;
						}
					}
				}
			}
			// Also check the default "Rest of the World" zone.
			$default_zone = new WC_Shipping_Zone( 0 );
			foreach ( $default_zone->get_shipping_methods() as $method ) {
				if ( 'free_shipping' === $method->id && 'yes' === $method->enabled ) {
					$min = isset( $method->min_amount ) ? (float) $method->min_amount : 0;
					if ( $min > 0 ) {
						$candidates[] = $min;
					}
				}
			}

			return empty( $candidates ) ? 0 : (float) min( $candidates );
		}

		/**
		 * Render the "Recently Viewed" strip — small thumbnail row above the bar.
		 *
		 * @param WC_Product[] $products
		 */
		private function render_recently_viewed_strip( $products ) {
			if ( empty( $products ) ) {
				return;
			}
			?>
			<div class="sz-sac__recent" aria-label="<?php esc_attr_e( 'Recently viewed', 'storzen' ); ?>">
				<button type="button" class="sz-sac__recent-toggle" aria-expanded="false" aria-controls="sz-sac-recent-list">
					<span class="sz-sac__recent-label"><?php esc_html_e( 'Recently viewed', 'storzen' ); ?></span>
					<span class="sz-sac__recent-count">(<?php echo (int) count( $products ); ?>)</span>
					<span class="sz-sac__recent-caret" aria-hidden="true">▾</span>
				</button>
				<ul id="sz-sac-recent-list" class="sz-sac__recent-list" hidden>
					<?php foreach ( $products as $p ) :
						$image_id = $p->get_image_id();
						$href     = get_permalink( $p->get_id() );
						?>
						<li>
							<a href="<?php echo esc_url( $href ); ?>" title="<?php echo esc_attr( $p->get_name() ); ?>">
								<?php if ( $image_id ) : ?>
									<?php echo wp_get_attachment_image( $image_id, 'thumbnail', false, array( 'alt' => esc_attr( $p->get_name() ), 'loading' => 'lazy' ) ); ?>
								<?php endif; ?>
								<span class="screen-reader-text"><?php echo esc_html( $p->get_name() ); ?></span>
							</a>
						</li>
					<?php endforeach; ?>
				</ul>
			</div>
			<?php
		}



		// ─────────────────────────────────────────────────────────────────
		// AJAX — Buy Now
		// ─────────────────────────────────────────────────────────────────

		/**
		 * AJAX: add product to cart and respond with checkout URL.
		 *
		 * POST: product_id, quantity, nonce
		 */
		public function ajax_buy_now() {
			$product_id = isset( $_POST['product_id'] ) ? absint( wp_unslash( $_POST['product_id'] ) ) : 0;
			$nonce      = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';

			if ( ! $product_id || ! wp_verify_nonce( $nonce, 'storzen_sac_' . $product_id ) ) {
				wp_send_json_error( array( 'message' => __( 'Invalid request.', 'storzen' ) ), 400 );
			}

			$product = wc_get_product( $product_id );
			if ( ! $product instanceof WC_Product || ! $product->is_purchasable() || ! $product->is_in_stock() ) {
				wp_send_json_error( array( 'message' => __( 'Product unavailable.', 'storzen' ) ), 400 );
			}

			// Variable products go through the variation form — Buy Now is for simple here.
			if ( $product->is_type( 'variable' ) ) {
				wp_send_json_error( array( 'message' => __( 'Please choose product options first.', 'storzen' ) ), 400 );
			}

			$quantity = isset( $_POST['quantity'] ) ? max( 1, absint( wp_unslash( $_POST['quantity'] ) ) ) : 1;

			if ( ! WC()->cart ) {
				wp_send_json_error( array( 'message' => __( 'Cart is unavailable.', 'storzen' ) ), 500);
			}

			$added = WC()->cart->add_to_cart( $product_id, $quantity );

			if ( ! $added ) {
				$notices = wc_get_notices( 'error' );
				$message = '';
				if ( ! empty( $notices ) && isset( $notices[0]['notice'] ) ) {
					$message = wp_strip_all_tags( $notices[0]['notice'] );
				}
				if ( '' === $message ) {
					$message = __( 'Could not add to cart.', 'storzen' );
				}
				wc_clear_notices();
				wp_send_json_error( array( 'message' => $message ), 400 );
			}

			$redirect = function_exists( 'wc_get_checkout_url' )
				? wc_get_checkout_url()
				: home_url( '/checkout/' );

			wp_send_json_success(
				array(
					'redirect'  => esc_url_raw( $redirect ),
					'cart_hash' => WC()->cart->get_cart_hash(),
				)
			);
		}

	} // end class

} // end class_exists
