<?php
/**
 * Storzen Module — Compare Products
 *
 * Lets customers add products to a side-by-side comparison table.
 *
 * Architecture:
 *   • Cookie-based storage (`storzen_compare_list` = comma-separated product IDs)
 *   • Capped at `max_products` (typically 4)
 *   • Compare button appears on shop cards (via storzen_shop_card_overlay) and single product pages
 *   • Sticky bar in footer shows current count + "Open" / "Clear" buttons
 *   • Compare table rendered via [storzen_compare] shortcode or assigned page
 *   • AJAX add/remove/clear endpoints (no full page refresh)
 *   • Highlight differences (rows where products differ get a tinted background)
 *   • Hide identical rows (rows where all products agree are skipped)
 *
 * Settings honored:
 *   max_products, button_label, added_label, button_position, icon_style,
 *   compare_page, fields_to_compare, highlight_diffs, hide_identical_rows,
 *   layout, sticky_bar, sticky_bar_position, open_in, accent_color
 *   (Pro settings collapse to defaults without a license)
 *
 * Hooks fired for theme/integration:
 *   storzen_compare_added     — when a product is added (passes product_id)
 *   storzen_compare_removed   — when removed
 *   storzen_compare_cleared   — when list cleared
 *   storzen_compare_table_row — filter on each row's value (row_key, value, product)
 *
 * @package storzen
 * @since   1.5.1
 */

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

if ( ! class_exists( 'Storzen_Module_Compare' ) ) {

	class Storzen_Module_Compare {

		const COOKIE_KEY  = 'storzen_compare_list';
		const SHORTCODE   = 'storzen_compare';
		const AJAX_ADD    = 'storzen_compare_add';
		const AJAX_REMOVE = 'storzen_compare_remove';
		const AJAX_CLEAR  = 'storzen_compare_clear';
		const NONCE       = 'storzen_compare_nonce';

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

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

			// Assets + footer (sticky bar).
			add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_assets' ) );
			add_action( 'wp_footer',          array( $this, 'render_sticky_bar' ), 25 );

			// Compare button on shop loop — both WC default + Storzen-native hooks.
			add_action( 'woocommerce_after_shop_loop_item_title', array( $this, 'render_button' ), 14 );
			// Storzen card overlay: icon-only variant matching cart / wishlist circles.
			add_action( 'storzen_shop_card_overlay',          array( $this, 'render_button_overlay' ), 15, 1 );

			// Compare button on single product — between price and ATC.
			add_action( 'woocommerce_single_product_summary',     array( $this, 'render_button' ), 35 );
			add_action( 'storzen_product_single_after_price', array( $this, 'render_button' ), 20, 1 );

			// Shortcode for the compare table.
			add_shortcode( self::SHORTCODE, array( $this, 'shortcode_render_table' ) );

			// Auto-inject the table on the assigned Compare page.
			add_filter( 'the_content', array( $this, 'maybe_inject_table' ), 9 );

			// AJAX endpoints (logged-in + guest).
			add_action( 'wp_ajax_'        . self::AJAX_ADD,    array( $this, 'ajax_add' ) );
			add_action( 'wp_ajax_nopriv_' . self::AJAX_ADD,    array( $this, 'ajax_add' ) );
			add_action( 'wp_ajax_'        . self::AJAX_REMOVE, array( $this, 'ajax_remove' ) );
			add_action( 'wp_ajax_nopriv_' . self::AJAX_REMOVE, array( $this, 'ajax_remove' ) );
			add_action( 'wp_ajax_'        . self::AJAX_CLEAR,  array( $this, 'ajax_clear' ) );
			add_action( 'wp_ajax_nopriv_' . self::AJAX_CLEAR,  array( $this, 'ajax_clear' ) );
			add_action( 'wp_ajax_storzen_compare_table',        array( $this, 'ajax_table' ) );
			add_action( 'wp_ajax_nopriv_storzen_compare_table', array( $this, 'ajax_table' ) );
		}

		// ─────────────────────────────────────────────────────────────────
		// Settings
		// ─────────────────────────────────────────────────────────────────

		private function get_settings() {
			if ( null !== $this->settings ) {
				return $this->settings;
			}

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

			$valid_positions = array( 'after-title', 'after-cart', 'on-image' );
			$valid_icons     = array( 'columns', 'plus', 'check', 'none' );
			$valid_layouts   = array( 'table', 'grid' );
			$valid_open      = array( 'modal', 'page' );
			$valid_sticky_p  = array( 'top', 'bottom' );

			$position = (string) storzen_module_setting( 'compare', 'button_position' );
			if ( ! in_array( $position, $valid_positions, true ) ) {
				$position = 'after-title';
			}

			$icon = (string) storzen_module_setting( 'compare', 'icon_style' );
			if ( ! in_array( $icon, $valid_icons, true ) ) {
				$icon = 'columns';
			}

			$layout = (string) storzen_module_setting( 'compare', 'layout' );
			if ( ! in_array( $layout, $valid_layouts, true ) ) {
				$layout = 'table';
			}

			$open = (string) storzen_module_setting( 'compare', 'open_in' );
			if ( ! in_array( $open, $valid_open, true ) ) {
				$open = 'modal';
			}

			$sticky_pos = (string) storzen_module_setting( 'compare', 'sticky_bar_position' );
			if ( ! in_array( $sticky_pos, $valid_sticky_p, true ) ) {
				$sticky_pos = 'bottom';
			}

			$btn_label   = (string) storzen_module_setting( 'compare', 'button_label' );
			$added_label = (string) storzen_module_setting( 'compare', 'added_label' );

			$fields = storzen_module_setting( 'compare', 'fields_to_compare' );
			if ( ! is_array( $fields ) || empty( $fields ) ) {
				$fields = array( 'image', 'price', 'rating', 'short_desc', 'add_to_cart' );
			}

			$accent = sanitize_hex_color( (string) storzen_module_setting( 'compare', 'accent_color' ) );
			if ( ! $accent ) {
				$accent = '#FF3636';
			}

			$this->settings = array(
				'max_products'        => max( 2, min( 6, (int) storzen_module_setting( 'compare', 'max_products' ) ?: 4 ) ),
				'button_label'        => $btn_label !== '' ? $btn_label : __( 'Compare', 'storzen' ),
				'added_label'         => $added_label !== '' ? $added_label : __( 'Added', 'storzen' ),
				'button_position'     => $position,
				'icon_style'          => $icon,
				'compare_page'        => (int) storzen_module_setting( 'compare', 'compare_page' ),
				'fields_to_compare'   => array_values( array_filter( array_map( 'sanitize_key', $fields ) ) ),
				'highlight_diffs'     => (bool) storzen_module_setting( 'compare', 'highlight_diffs' ),
				'hide_identical_rows' => (bool) storzen_module_setting( 'compare', 'hide_identical_rows' ),
				'layout'              => $layout,
				'sticky_bar'          => (bool) storzen_module_setting( 'compare', 'sticky_bar' ),
				'sticky_bar_position' => $sticky_pos,
				'open_in'             => $open,
				'accent_color'        => $accent,
				// Pro (gated)
				'pro_shareable_url'      => $is_pro && (bool) storzen_module_setting( 'compare', 'pro_shareable_url' ),
				'pro_pdf_export'         => $is_pro && (bool) storzen_module_setting( 'compare', 'pro_pdf_export' ),
				'pro_print_view'         => $is_pro && (bool) storzen_module_setting( 'compare', 'pro_print_view' ),
				'pro_show_competitors'   => $is_pro && (bool) storzen_module_setting( 'compare', 'pro_show_competitors' ),
				'pro_pre_filled_compare' => $is_pro && (bool) storzen_module_setting( 'compare', 'pro_pre_filled_compare' ),
				'is_pro_active'          => $is_pro,
			);
			return $this->settings;
		}

		// ─────────────────────────────────────────────────────────────────
		// Storage helpers — cookie list of product IDs
		// ─────────────────────────────────────────────────────────────────

		/**
		 * Read the current compare list from cookie.
		 *
		 * @return int[] Product IDs (filtered, validated, max 6).
		 */
		public function get_list() {
			$raw = isset( $_COOKIE[ self::COOKIE_KEY ] )
				? sanitize_text_field( wp_unslash( $_COOKIE[ self::COOKIE_KEY ] ) )
				: '';
			if ( '' === $raw ) {
				return array();
			}
			$ids = array_filter( array_map( 'absint', explode( ',', $raw ) ) );
			$ids = array_values( array_unique( $ids ) );
			// Hard cap to defend against malicious cookies.
			return array_slice( $ids, 0, 6 );
		}

		/**
		 * Write a new list to the cookie. Caps at max_products.
		 *
		 * @param int[] $ids
		 */
		private function set_list( array $ids ) {
			$s   = $this->get_settings();
			$ids = array_filter( array_map( 'absint', $ids ) );
			$ids = array_values( array_unique( $ids ) );
			$ids = array_slice( $ids, 0, (int) $s['max_products'] );

			$value = implode( ',', $ids );
			// 30 days — `wc_setcookie` uses WC's own helper which respects
			// COOKIEPATH / COOKIEDOMAIN and avoids httpOnly so JS can also read.
			if ( function_exists( 'wc_setcookie' ) ) {
				wc_setcookie( self::COOKIE_KEY, $value, time() + ( 30 * DAY_IN_SECONDS ) );
			} else {
				setcookie( self::COOKIE_KEY, $value, time() + ( 30 * DAY_IN_SECONDS ), defined( 'COOKIEPATH' ) ? COOKIEPATH : '/' );
			}
			// Reflect the new value into $_COOKIE so subsequent reads in the
			// same request see the update (PHP cookies normally update next request).
			$_COOKIE[ self::COOKIE_KEY ] = $value;
		}

		// ─────────────────────────────────────────────────────────────────
		// Asset enqueue
		// ─────────────────────────────────────────────────────────────────

		public function enqueue_assets() {
			if ( is_admin() ) {
				return;
			}

			$s = $this->get_settings();

			wp_enqueue_style(
				'storzen-compare',
				STORZEN_ROOT_URL . 'assets/css/modules/compare.css',
				array(),
				STORZEN_VERSION
			);
			// Accent color is the only dynamic CSS — inline it.
			wp_add_inline_style(
				'storzen-compare',
				':root{--storzen-compare-accent:' . $s['accent_color'] . ';}'
			);

			wp_enqueue_script(
				'storzen-compare',
				STORZEN_ROOT_URL . 'assets/js/modules/compare.js',
				array( 'jquery' ),
				STORZEN_VERSION,
				true
			);
			wp_localize_script(
				'storzen-compare',
				'szCompare',
				array(
					'ajaxUrl'       => admin_url( 'admin-ajax.php' ),
					'nonce'         => wp_create_nonce( self::NONCE ),
					'list'          => $this->get_list(),
					'maxProducts'   => (int) $s['max_products'],
					'openIn'        => $s['open_in'],
					'comparePageUrl'=> $s['compare_page'] ? get_permalink( $s['compare_page'] ) : '',
					'i18n'          => array(
						'add'         => $s['button_label'],
						'added'       => $s['added_label'],
						'full'        => sprintf(
							/* translators: %d max products */
							__( 'You can compare up to %d products at a time.', 'storzen' ),
							(int) $s['max_products']
						),
						'noProducts'  => __( 'No products in compare yet.', 'storzen' ),
						'open'        => __( 'Open compare', 'storzen' ),
						'clear'       => __( 'Clear all', 'storzen' ),
						'remove'      => __( 'Remove', 'storzen' ),
						'error'       => __( 'Could not update compare list. Please try again.', 'storzen' ),
					),
				)
			);
		}

		// ─────────────────────────────────────────────────────────────────
		// Compare button on product cards + single product
		// ─────────────────────────────────────────────────────────────────

		/**
		 * Render the Compare button. Listens on both WC default hooks (passes
		 * no args, reads global $product) and Storzen-native hooks (pass $product).
		 *
		 * @param WC_Product|null $product_arg
		 * @param string          $force_position Optional. 'overlay' renders a
		 *                        compact icon-only button (text label hidden) for
		 *                        the Storzen card overlay area.
		 */
		public function render_button( $product_arg = null, $force_position = '' ) {
			if ( class_exists( 'Elementor\Storzen_Archive_Grid' ) ) {
				$ctx = \Elementor\Storzen_Archive_Grid::is_module_active( 'compare' );
				if ( false === $ctx ) {
					return;
				}
			}
			$product = ( $product_arg instanceof WC_Product ) ? $product_arg : ( $GLOBALS['product'] ?? null );
			if ( ! $product instanceof WC_Product ) {
				return;
			}

			$s    = $this->get_settings();
			$id   = (int) $product->get_id();
			$list = $this->get_list();
			$is_added = in_array( $id, $list, true );

			$icon_map = array(
				'columns' => 'fas fa-columns',
				'plus'    => 'fas fa-plus',
				'check'   => 'fas fa-check',
				'none'    => '',
			);
			$icon_class = $icon_map[ $s['icon_style'] ] ?? 'fas fa-columns';

			$is_overlay = ( 'overlay' === $force_position );

			// Overlay: always show an icon; fall back to columns if none configured.
			if ( $is_overlay && '' === $icon_class ) {
				$icon_class = 'fas fa-columns';
			}

			$label   = $is_added ? $s['added_label'] : $s['button_label'];
			$classes = 'sz-compare-btn';
			if ( $is_overlay ) {
				$classes .= ' sz-compare-btn--overlay';
			}
			if ( $is_added ) {
				$classes .= ' is-added';
			}
			?>
			<button
				type="button"
				class="<?php echo esc_attr( $classes ); ?>"
				data-product-id="<?php echo esc_attr( (string) $id ); ?>"
				data-label-add="<?php echo esc_attr( $s['button_label'] ); ?>"
				data-label-added="<?php echo esc_attr( $s['added_label'] ); ?>"
				title="<?php echo esc_attr( $label ); ?>"
				aria-pressed="<?php echo $is_added ? 'true' : 'false'; ?>"
			>
				<?php if ( '' !== $icon_class ) : ?>
					<i class="<?php echo esc_attr( $icon_class ); ?>" aria-hidden="true"></i>
				<?php endif; ?>
				<?php if ( ! $is_overlay ) : ?>
					<span class="sz-compare-btn__label"><?php echo esc_html( $label ); ?></span>
				<?php endif; ?>
			</button>
			<?php
		}

		/**
		 * Render the Compare trigger inside the Storzen card overlay area as a
		 * compact icon-only circle (matching the cart / wishlist buttons).
		 *
		 * @param WC_Product|null $product_arg Product passed by the hook.
		 */
		public function render_button_overlay( $product_arg = null ) {
			$this->render_button( $product_arg, 'overlay' );
		}

		// ─────────────────────────────────────────────────────────────────
		// Sticky bar — appears when compare list is non-empty
		// ─────────────────────────────────────────────────────────────────

		public function render_sticky_bar() {
			if ( is_admin() ) {
				return;
			}
			$s = $this->get_settings();
			if ( ! $s['sticky_bar'] ) {
				return;
			}

			$list  = $this->get_list();
			$count = count( $list );
			$classes = 'sz-compare-bar sz-compare-bar--' . $s['sticky_bar_position'];
			if ( 0 === $count ) {
				$classes .= ' is-empty';
			}
			$open_url = ( 'page' === $s['open_in'] && $s['compare_page'] )
				? get_permalink( $s['compare_page'] )
				: '#';
			?>
			<div
				class="<?php echo esc_attr( $classes ); ?>"
				role="region"
				aria-label="<?php esc_attr_e( 'Compare bar', 'storzen' ); ?>"
			>
				<div class="sz-compare-bar__inner">
					<span class="sz-compare-bar__label">
						<?php esc_html_e( 'Compare', 'storzen' ); ?>
						<span class="sz-compare-bar__count">(<span data-sz-compare-count><?php echo (int) $count; ?></span>)</span>
					</span>
					<div class="sz-compare-bar__actions">
						<button type="button" class="sz-compare-bar__open" data-sz-compare-open data-href="<?php echo esc_url( $open_url ); ?>">
							<?php esc_html_e( 'Open', 'storzen' ); ?>
						</button>
						<button type="button" class="sz-compare-bar__clear" data-sz-compare-clear>
							<?php esc_html_e( 'Clear', 'storzen' ); ?>
						</button>
					</div>
				</div>
			</div>

			<?php if ( 'modal' === $s['open_in'] ) : ?>
				<div class="sz-compare-modal" id="sz-compare-modal" role="dialog" aria-modal="true" aria-hidden="true">
					<div class="sz-compare-modal__overlay" data-sz-compare-close></div>
					<div class="sz-compare-modal__panel">
						<header class="sz-compare-modal__header">
							<h2 class="sz-compare-modal__title"><?php esc_html_e( 'Product Comparison', 'storzen' ); ?></h2>
							<button type="button" class="sz-compare-modal__close" data-sz-compare-close aria-label="<?php esc_attr_e( 'Close', 'storzen' ); ?>">&times;</button>
						</header>
						<div class="sz-compare-modal__body" data-sz-compare-body>
							<?php // Filled by JS via AJAX when opened. Empty on first paint. ?>
						</div>
					</div>
				</div>
			<?php endif; ?>
			<?php
		}

		// ─────────────────────────────────────────────────────────────────
		// Comparison table rendering
		// ─────────────────────────────────────────────────────────────────

		/**
		 * Shortcode [storzen_compare] — renders the comparison table.
		 *
		 * @return string HTML
		 */
		public function shortcode_render_table() {
			return $this->render_table();
		}

		/**
		 * Filter `the_content` to inject the table on the assigned Compare page.
		 * Only fires on the main query for the configured page; no-op everywhere else.
		 *
		 * @param string $content
		 * @return string
		 */
		public function maybe_inject_table( $content ) {
			$s = $this->get_settings();
			if ( ! $s['compare_page'] ) {
				return $content;
			}
			if ( ! is_singular() || ! is_main_query() || ! in_the_loop() ) {
				return $content;
			}
			if ( (int) get_the_ID() !== (int) $s['compare_page'] ) {
				return $content;
			}
			// If author already included the shortcode in the page body, skip
			// to avoid double rendering.
			if ( has_shortcode( $content, self::SHORTCODE ) ) {
				return $content;
			}
			return $content . $this->render_table();
		}

		/**
		 * Build the comparison table HTML.
		 *
		 * @return string
		 */
		public function render_table() {
			$s   = $this->get_settings();
			$ids = $this->get_list();

			if ( empty( $ids ) ) {
				return '<div class="sz-compare-empty">' . esc_html__( 'No products in compare yet. Add some from the shop to see them side by side.', 'storzen' ) . '</div>';
			}

			$products = array();
			foreach ( $ids as $id ) {
				$p = wc_get_product( $id );
				if ( $p instanceof WC_Product && $p->is_visible() ) {
					$products[] = $p;
				}
			}
			if ( empty( $products ) ) {
				return '<div class="sz-compare-empty">' . esc_html__( 'The products in your compare list are no longer available.', 'storzen' ) . '</div>';
			}

			$rows         = $this->get_active_rows();
			$hide_ident   = (bool) $s['hide_identical_rows'];
			$highlight    = (bool) $s['highlight_diffs'];

			ob_start();
			?>
			<div class="sz-compare-wrap sz-compare-layout--<?php echo esc_attr( $s['layout'] ); ?>">
				<table class="sz-compare-table">
					<thead>
						<tr>
							<th class="sz-compare-table__label-col" scope="col">&nbsp;</th>
							<?php foreach ( $products as $p ) : ?>
								<th scope="col" class="sz-compare-table__product-col" data-product-id="<?php echo esc_attr( $p->get_id() ); ?>">
									<button type="button" class="sz-compare-table__remove" data-sz-compare-remove="<?php echo esc_attr( $p->get_id() ); ?>" aria-label="<?php esc_attr_e( 'Remove', 'storzen' ); ?>">&times;</button>
									<a class="sz-compare-table__name" href="<?php echo esc_url( get_permalink( $p->get_id() ) ); ?>">
										<?php echo esc_html( $p->get_name() ); ?>
									</a>
								</th>
							<?php endforeach; ?>
						</tr>
					</thead>
					<tbody>
						<?php foreach ( $rows as $row_key => $row_label ) :
							// Collect each product's value for this row first so we can
							// detect identical rows + apply highlighting.
							$values = array();
							foreach ( $products as $p ) {
								$values[ $p->get_id() ] = $this->get_row_value( $row_key, $p );
							}

							// Hide identical rows when the option is on.
							$plain_values = array_map( array( $this, 'strip_html_for_compare' ), $values );
							$is_identical = count( array_unique( $plain_values ) ) <= 1;
							if ( $hide_ident && $is_identical && count( $products ) > 1 ) {
								continue;
							}

							$row_classes = 'sz-compare-table__row sz-compare-table__row--' . esc_attr( $row_key );
							if ( $highlight && ! $is_identical && count( $products ) > 1 ) {
								$row_classes .= ' is-different';
							}
						?>
							<tr class="<?php echo esc_attr( $row_classes ); ?>">
								<th scope="row" class="sz-compare-table__label-col"><?php echo esc_html( $row_label ); ?></th>
								<?php foreach ( $products as $p ) : ?>
									<td>
										<?php
										echo wp_kses_post(
											apply_filters( 'storzen_compare_table_row', $values[ $p->get_id() ], $row_key, $p )
										);
										?>
									</td>
								<?php endforeach; ?>
							</tr>
						<?php endforeach; ?>
					</tbody>
				</table>
			</div>
			<?php
			return ob_get_clean();
		}

		/**
		 * Get the user-selected rows (intersected with the available field map).
		 *
		 * @return array<string, string>  row_key => translated label
		 */
		private function get_active_rows() {
			$all_rows = array(
				'image'       => __( 'Product Image', 'storzen' ),
				'price'       => __( 'Price', 'storzen' ),
				'rating'      => __( 'Rating', 'storzen' ),
				'sku'         => __( 'SKU', 'storzen' ),
				'stock'       => __( 'Stock Status', 'storzen' ),
				'short_desc'  => __( 'Description', 'storzen' ),
				'categories'  => __( 'Categories', 'storzen' ),
				'tags'        => __( 'Tags', 'storzen' ),
				'attributes'  => __( 'Attributes', 'storzen' ),
				'weight'      => __( 'Weight', 'storzen' ),
				'dimensions'  => __( 'Dimensions', 'storzen' ),
				'add_to_cart' => __( 'Add to Cart', 'storzen' ),
			);
			$s        = $this->get_settings();
			$selected = (array) $s['fields_to_compare'];
			$out      = array();
			// Preserve the user-chosen order if non-default; fall back to all_rows order.
			$order = ! empty( $selected ) ? $selected : array_keys( $all_rows );
			foreach ( $order as $key ) {
				if ( isset( $all_rows[ $key ] ) ) {
					$out[ $key ] = $all_rows[ $key ];
				}
			}
			return $out;
		}

		/**
		 * Render the value for a given row + product.
		 *
		 * @param string     $row_key
		 * @param WC_Product $product
		 * @return string HTML
		 */
		private function get_row_value( $row_key, $product ) {
			switch ( $row_key ) {
				case 'image':
					$thumb_id = $product->get_image_id();
					if ( $thumb_id ) {
						$img = wp_get_attachment_image(
							$thumb_id,
							'woocommerce_thumbnail',
							false,
							array( 'class' => 'sz-compare-table__img', 'loading' => 'lazy', 'alt' => esc_attr( $product->get_name() ) )
						);
						return '<a href="' . esc_url( get_permalink( $product->get_id() ) ) . '">' . $img . '</a>';
					}
					return '<span class="sz-compare-table__missing">—</span>';

				case 'price':
					return (string) $product->get_price_html();

				case 'rating':
					$avg   = (float) $product->get_average_rating();
					$count = (int) $product->get_review_count();
					if ( $count <= 0 ) {
						return '<span class="sz-compare-table__missing">—</span>';
					}
					return wc_get_rating_html( $avg, $count );

				case 'sku':
					$sku = $product->get_sku();
					return $sku ? esc_html( $sku ) : '<span class="sz-compare-table__missing">—</span>';

				case 'stock':
					$status = $product->get_stock_status();
					if ( 'instock' === $status ) {
						return '<span class="sz-compare-stock sz-compare-stock--in">' . esc_html__( 'In stock', 'storzen' ) . '</span>';
					}
					if ( 'onbackorder' === $status ) {
						return '<span class="sz-compare-stock sz-compare-stock--back">' . esc_html__( 'On backorder', 'storzen' ) . '</span>';
					}
					return '<span class="sz-compare-stock sz-compare-stock--out">' . esc_html__( 'Out of stock', 'storzen' ) . '</span>';

				case 'short_desc':
					$desc = $product->get_short_description();
					if ( ! $desc ) {
						return '<span class="sz-compare-table__missing">—</span>';
					}
					return wp_kses_post( wpautop( $desc ) );

				case 'categories':
					$terms = wc_get_product_category_list( $product->get_id(), ', ' );
					return $terms ?: '<span class="sz-compare-table__missing">—</span>';

				case 'tags':
					$terms = wc_get_product_tag_list( $product->get_id(), ', ' );
					return $terms ?: '<span class="sz-compare-table__missing">—</span>';

				case 'attributes':
					$attrs = $product->get_attributes();
					if ( empty( $attrs ) ) {
						return '<span class="sz-compare-table__missing">—</span>';
					}
					$lines = array();
					foreach ( $attrs as $attr ) {
						/** @var WC_Product_Attribute $attr */
						$name  = wc_attribute_label( $attr->get_name(), $product );
						$value = '';
						if ( $attr->is_taxonomy() ) {
							$terms = wc_get_product_terms( $product->get_id(), $attr->get_name(), array( 'fields' => 'names' ) );
							$value = implode( ', ', $terms );
						} else {
							$value = implode( ', ', (array) $attr->get_options() );
						}
						if ( $value ) {
							$lines[] = '<strong>' . esc_html( $name ) . ':</strong> ' . esc_html( $value );
						}
					}
					return $lines ? implode( '<br>', $lines ) : '<span class="sz-compare-table__missing">—</span>';

				case 'weight':
					$w = $product->get_weight();
					return $w ? esc_html( wc_format_weight( $w ) ) : '<span class="sz-compare-table__missing">—</span>';

				case 'dimensions':
					$d = wc_format_dimensions( $product->get_dimensions( false ) );
					return $d && 'N/A' !== $d ? esc_html( $d ) : '<span class="sz-compare-table__missing">—</span>';

				case 'add_to_cart':
					// Use WC's stock URL + label; avoids re-implementing variation handling.
					$url   = $product->add_to_cart_url();
					$label = $product->add_to_cart_text();
					return sprintf(
						'<a href="%s" class="button sz-compare-atc">%s</a>',
						esc_url( $url ),
						esc_html( $label )
					);
			}
			return '';
		}

		/**
		 * Strip HTML for the "identical rows" detection — we want to compare
		 * the human-visible value, not markup differences.
		 *
		 * @param string $html
		 * @return string
		 */
		private function strip_html_for_compare( $html ) {
			return trim( wp_strip_all_tags( (string) $html ) );
		}

		// ─────────────────────────────────────────────────────────────────
		// AJAX endpoints
		// ─────────────────────────────────────────────────────────────────

		public function ajax_add() {
			$this->verify_nonce();
			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above in verify_nonce().
			$id = isset( $_POST['product_id'] ) ? absint( wp_unslash( $_POST['product_id'] ) ) : 0;
			if ( ! $id || ! wc_get_product( $id ) ) {
				wp_send_json_error( array( 'message' => __( 'Invalid product.', 'storzen' ) ), 400 );
			}

			$s    = $this->get_settings();
			$list = $this->get_list();

			if ( in_array( $id, $list, true ) ) {
				wp_send_json_success(
					array(
						'list'  => $list,
						'count' => count( $list ),
						'state' => 'already-in',
					)
				);
			}

			if ( count( $list ) >= (int) $s['max_products'] ) {
				wp_send_json_error(
					array(
						'message' => sprintf(
							/* translators: %d max */
							__( 'You can compare up to %d products at a time.', 'storzen' ),
							(int) $s['max_products']
						),
						'list'    => $list,
					),
					409
				);
			}

			$list[] = $id;
			$this->set_list( $list );
			do_action( 'storzen_compare_added', $id );

			wp_send_json_success(
				array(
					'list'  => $list,
					'count' => count( $list ),
					'state' => 'added',
				)
			);
		}

		public function ajax_remove() {
			$this->verify_nonce();
			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verified above in verify_nonce().
			$id = isset( $_POST['product_id'] ) ? absint( wp_unslash( $_POST['product_id'] ) ) : 0;

			$list = $this->get_list();
			$list = array_values( array_diff( $list, array( $id ) ) );
			$this->set_list( $list );

			do_action( 'storzen_compare_removed', $id );

			wp_send_json_success(
				array(
					'list'  => $list,
					'count' => count( $list ),
					'table' => $this->render_table(), // Fresh HTML for modal refresh
				)
			);
		}

		public function ajax_clear() {
			$this->verify_nonce();
			$this->set_list( array() );
			do_action( 'storzen_compare_cleared' );
			wp_send_json_success(
				array(
					'list'  => array(),
					'count' => 0,
					'table' => $this->render_table(),
				)
			);
		}

		/**
		 * Endpoint to fetch the rendered table for modal opens.
		 * Re-uses ajax_remove's response structure but no mutation.
		 */
		public function ajax_table() {
			$this->verify_nonce();
			wp_send_json_success(
				array(
					'list'  => $this->get_list(),
					'count' => count( $this->get_list() ),
					'table' => $this->render_table(),
				)
			);
		}

		/**
		 * Verify the AJAX nonce sent in POST. Sends a 403 error and exits on failure.
		 */
		private function verify_nonce() {
			$nonce = isset( $_POST['nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['nonce'] ) ) : '';
			if ( ! wp_verify_nonce( $nonce, self::NONCE ) ) {
				wp_send_json_error( array( 'message' => __( 'Security check failed. Please refresh and try again.', 'storzen' ) ), 403 );
			}
		}

	} // end class

} // end class_exists
