<?php
/**
 * Storzen Module — Variation Swatches
 *
 * Replaces WooCommerce's plain <select> dropdowns for variable product
 * attributes with color, image, button, or (Pro) SVG swatches.
 *
 * Features:
 *   • Color / image / button / auto swatch types
 *   • Per-attribute-term meta UI on the WP taxonomy term edit screens
 *   • Out-of-stock variation styling (crossed / faded / hidden / none)
 *   • Tooltip on hover, configurable position
 *   • Auto-select first available variation
 *   • Update main product image / gallery on swatch click
 *   • Optional swatches on shop / category cards
 *   • Pro: variation gallery cycling, animations, custom SVG shapes, custom CSS
 *
 * Compatibility:
 *   • Classic + Block themes
 *   • HPOS — module is presentation-only, no order data touched
 *   • Translation-ready ('storzen' text domain)
 *   • Honors STORZEN_IS_PRO for Pro-only settings
 *
 * Term meta keys (per attribute term):
 *   _storzen_swatch_type   string  color|image|button|svg
 *   _storzen_swatch_color  string  #RRGGBB
 *   _storzen_swatch_image  int     attachment ID
 *   _storzen_swatch_svg    string  raw SVG (Pro only)
 *
 * @package storzen
 * @since   1.3.0
 */

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

if ( ! class_exists( 'Storzen_Module_Variation_Swatches' ) ) {

	class Storzen_Module_Variation_Swatches {

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

		/** @var bool Whether assets were already enqueued this request */
		private $assets_enqueued = false;

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

			// ── Frontend: swatch rendering ─────────────────────────────
			add_filter(
				'woocommerce_dropdown_variation_attribute_options_html',
				array( $this, 'render_swatches_html' ),
				10,
				2
			);

			// Conditional asset loading on single product pages.
			add_action( 'wp_enqueue_scripts', array( $this, 'maybe_enqueue_assets' ) );

			// ── Shop / category cards ──────────────────────────────────
			$show_on = (array) $this->get( 'show_on', array( 'single', 'shop' ) );
			if ( in_array( 'shop', $show_on, true ) ) {
				// Default WC theme hook.
				add_action(
					'woocommerce_after_shop_loop_item_title',
					array( $this, 'render_shop_swatches' ),
					15
				);
				// Storzen-native: fired by imported Elementor grid templates.
				add_action(
					'storzen_shop_card_after_title',
					array( $this, 'render_shop_swatches' ),
					15,
					1
				);
			}

			// ── Admin: per-term swatch meta UI ─────────────────────────
			if ( is_admin() ) {
				$this->register_term_meta_ui();
				add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_assets' ) );
			}
		}

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

		/**
		 * Resolve module settings once per request, with sensible defaults.
		 * Pro settings collapse to their defaults when STORZEN_IS_PRO !== 'yes'.
		 *
		 * @return array
		 */
		private function resolve_settings() {
			if ( null !== $this->settings ) {
				return $this->settings;
			}

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

			$this->settings = array(
				// General
				'swatch_type'           => (string) storzen_module_setting( 'variation_swatches', 'swatch_type' ),
				'show_on'               => (array) storzen_module_setting( 'variation_swatches', 'show_on' ),
				'show_label'            => (bool) storzen_module_setting( 'variation_swatches', 'show_label' ),
				'attribute_image_size'  => (string) storzen_module_setting( 'variation_swatches', 'attribute_image_size' ),

				// Display
				'swatch_size'           => max( 16, (int) storzen_module_setting( 'variation_swatches', 'swatch_size' ) ),
				'shape'                 => (string) storzen_module_setting( 'variation_swatches', 'shape' ),
				'spacing'               => max( 0, (int) storzen_module_setting( 'variation_swatches', 'spacing' ) ),
				'selected_style'        => (string) storzen_module_setting( 'variation_swatches', 'selected_style' ),
				'accent_color'          => $this->sanitize_color( storzen_module_setting( 'variation_swatches', 'accent_color' ), '#FF3636' ),
				'show_tooltip'          => (bool) storzen_module_setting( 'variation_swatches', 'show_tooltip' ),
				'tooltip_position'      => (string) storzen_module_setting( 'variation_swatches', 'tooltip_position' ),

				// Behavior
				'out_of_stock_style'    => (string) storzen_module_setting( 'variation_swatches', 'out_of_stock_style' ),
				'auto_select_first'     => (bool) storzen_module_setting( 'variation_swatches', 'auto_select_first' ),
				'update_main_image'     => (bool) storzen_module_setting( 'variation_swatches', 'update_main_image' ),
				'show_price_change'     => (bool) storzen_module_setting( 'variation_swatches', 'show_price_change' ),

				// Pro — only effective if licensed
				'pro_variation_gallery' => $is_pro && (bool) storzen_module_setting( 'variation_swatches', 'pro_variation_gallery' ),
				'pro_animation'         => $is_pro ? (string) storzen_module_setting( 'variation_swatches', 'pro_animation' ) : 'fade',
				'pro_bulk_manager'      => $is_pro && (bool) storzen_module_setting( 'variation_swatches', 'pro_bulk_manager' ),
				'pro_custom_shapes'     => $is_pro && (bool) storzen_module_setting( 'variation_swatches', 'pro_custom_shapes' ),
				'pro_custom_css'        => $is_pro ? (string) storzen_module_setting( 'variation_swatches', 'pro_custom_css' ) : '',

				'is_pro_active'         => $is_pro,
			);

			return $this->settings;
		}

		/**
		 * Convenience getter with default fallback.
		 *
		 * @param string $key     Settings key
		 * @param mixed  $default Returned if the setting is empty
		 * @return mixed
		 */
		private function get( $key, $default = '' ) {
			$s = $this->resolve_settings();
			return ( isset( $s[ $key ] ) && '' !== $s[ $key ] && null !== $s[ $key ] )
				? $s[ $key ]
				: $default;
		}

		/**
		 * Sanitize a hex color with fallback.
		 *
		 * @param mixed  $raw
		 * @param string $fallback
		 * @return string
		 */
		private function sanitize_color( $raw, $fallback = '' ) {
			$clean = sanitize_hex_color( (string) $raw );
			return $clean ? $clean : $fallback;
		}

		// ─────────────────────────────────────────────────────────────────
		// Frontend asset enqueuing
		// ─────────────────────────────────────────────────────────────────

		public function maybe_enqueue_assets() {
			if ( is_admin() ) {
				return;
			}
			$show_on = (array) $this->get( 'show_on', array( 'single', 'shop' ) );
			$should_load =
				( in_array( 'single', $show_on, true ) && is_product() ) ||
				( in_array( 'shop',   $show_on, true ) && ( is_shop() || is_product_category() || is_product_tag() ) ) ||
				class_exists( '\Elementor\Plugin' ); // always load on Elementor pages

			if ( ! $should_load ) {
				return;
			}

			$this->enqueue_assets();
		}

		/**
		 * Enqueue swatch CSS, dynamic inline CSS, and JS. Idempotent.
		 */
		public function enqueue_assets() {
			if ( $this->assets_enqueued ) {
				return;
			}
			$this->assets_enqueued = true;

			$base_url = defined( 'STORZEN_ROOT_URL' ) ? STORZEN_ROOT_URL : plugin_dir_url( dirname( dirname( __FILE__ ) ) );
			$version  = defined( 'STORZEN_VERSION' ) ? STORZEN_VERSION : '1.0.0';

			wp_enqueue_style(
				'storzen-variation-swatches',
				$base_url . 'assets/css/modules/variation-swatches.css',
				array(),
				$version
			);
			wp_add_inline_style( 'storzen-variation-swatches', $this->build_inline_css() );

			wp_enqueue_script(
				'storzen-variation-swatches',
				$base_url . 'assets/js/modules/variation-swatches.js',
				array( 'jquery' ),
				$version,
				true
			);
			wp_localize_script(
				'storzen-variation-swatches',
				'StorzenSwatchData',
				array(
					'autoSelectFirst'    => (bool) $this->get( 'auto_select_first', false ),
					'updateMainImage'    => (bool) $this->get( 'update_main_image', true ),
					'animation'          => (string) $this->get( 'pro_animation', 'fade' ),
					'showPriceChange'    => (bool) $this->get( 'show_price_change', true ),
					'variationGallery'   => (bool) $this->get( 'pro_variation_gallery', false ),
					'outOfStockStyle'    => (string) $this->get( 'out_of_stock_style', 'crossed' ),
					'tooltipPosition'    => (string) $this->get( 'tooltip_position', 'top' ),
				)
			);
		}

		/**
		 * Build the dynamic CSS that reflects user-chosen size, shape, color, etc.
		 * Kept on the lean side — most static rules live in the .css file.
		 *
		 * @return string
		 */
		private function build_inline_css() {
			$size      = (int) $this->get( 'swatch_size', 28 );
			$spacing   = (int) $this->get( 'spacing', 6 );
			$accent    = $this->sanitize_color( $this->get( 'accent_color', '#FF3636' ), '#FF3636' );
			$shape     = (string) $this->get( 'shape', 'rounded' );
			$radius    = ( 'circle' === $shape ) ? '50%' : ( ( 'square' === $shape ) ? '2px' : '6px' );
			$custom    = (string) $this->get( 'pro_custom_css', '' );

			$css  = ":root{";
			$css .= "--storzen-swatch-size:{$size}px;";
			$css .= "--storzen-swatch-spacing:{$spacing}px;";
			$css .= "--storzen-swatch-radius:{$radius};";
			$css .= "--storzen-swatch-accent:{$accent};";
			$css .= "}";

			// Pro: arbitrary custom CSS (stripped of <style> tags + sanitized).
			if ( $custom ) {
				// Strip dangerous chars; CSS doesn't need < > so this is safe.
				$custom = wp_strip_all_tags( $custom );
				$css   .= "\n/* User custom CSS */\n" . $custom;
			}

			return $css;
		}

		// ─────────────────────────────────────────────────────────────────
		// Swatch rendering — single product page
		// ─────────────────────────────────────────────────────────────────

		/**
		 * Replace the default <select> with a swatch UI for variation attributes.
		 * Filter signature is set by WooCommerce.
		 *
		 * @param string $html Original dropdown HTML.
		 * @param array  $args WooCommerce's args bundle (options, attribute, product, etc).
		 * @return string Swatch HTML or original fallback.
		 */
		public function render_swatches_html( $html, $args ) {
			// Respect per-widget variation_swatches toggle when inside an Archive Grid render.
			if ( class_exists( 'Elementor\Storzen_Archive_Grid' ) ) {
				$ctx = \Elementor\Storzen_Archive_Grid::is_module_active( 'variation_swatches' );
				if ( false === $ctx ) {
					return $html;
				}
			}

			$options   = isset( $args['options'] )   ? (array) $args['options'] : array();
			$attribute = isset( $args['attribute'] ) ? (string) $args['attribute'] : '';
			$product   = isset( $args['product'] )   ? $args['product'] : null;

			if ( empty( $options ) || empty( $attribute ) || ! $product instanceof WC_Product ) {
				return $html;
			}

			// Determine the swatch type for this attribute.
			$type = $this->resolve_attribute_swatch_type( $attribute );

			// Button is the safest fallback — works without any per-term meta.
			$is_taxonomy = taxonomy_exists( $attribute );
			$selected    = isset( $args['selected'] ) ? (string) $args['selected'] : '';

			// Get the list of swatch entries: each with value, label, swatch payload.
			$entries = $this->build_swatch_entries( $attribute, $options, $type, $is_taxonomy );
			if ( empty( $entries ) ) {
				return $html;
			}

			// Out-of-stock detection per variation/attribute value.
			$disabled_values = $this->find_disabled_attribute_values( $product, $attribute );

			$this->enqueue_assets(); // Defensive — covers themes that filter early.

			$attr_label = wc_attribute_label( $attribute, $product );

			ob_start();
			?>
			<div class="storzen-swatches storzen-swatches--<?php echo esc_attr( $type ); ?>"
				data-attribute_name="attribute_<?php echo esc_attr( sanitize_title( $attribute ) ); ?>"
				data-attribute="<?php echo esc_attr( $attribute ); ?>"
				role="radiogroup"
				aria-label="<?php echo esc_attr( $attr_label ); ?>"
			>
				<?php foreach ( $entries as $entry ) :
					$value       = $entry['value'];
					$label       = $entry['label'];
					$is_selected = ( $selected === $value );
					$is_disabled = in_array( $value, $disabled_values, true );

					$classes  = 'storzen-swatch';
					$classes .= ' storzen-swatch--' . $type;
					if ( $is_selected ) {
						$classes .= ' is-selected';
					}
					if ( $is_disabled ) {
						$classes .= ' is-out-of-stock is-oos-' . esc_attr( $this->get( 'out_of_stock_style', 'crossed' ) );
					}
					?>
					<span class="<?php echo esc_attr( $classes ); ?>"
						role="radio"
						tabindex="<?php echo $is_disabled ? '-1' : '0'; ?>"
						aria-checked="<?php echo $is_selected ? 'true' : 'false'; ?>"
						aria-label="<?php echo esc_attr( $label ); ?>"
						data-value="<?php echo esc_attr( $value ); ?>"
						<?php if ( $this->get( 'show_tooltip', true ) ) : ?>
							data-tooltip="<?php echo esc_attr( $label ); ?>"
						<?php endif; ?>
					>
						<?php echo $this->render_swatch_visual( $entry, $type ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- sub-method handles all escaping ?>
					</span>
				<?php endforeach; ?>
			</div>

			<?php if ( $this->get( 'show_label', true ) ) : ?>
				<span class="storzen-swatch-selected-label" data-attribute="<?php echo esc_attr( $attribute ); ?>" data-label-prefix="<?php echo esc_attr( $attr_label . ': ' ); ?>" aria-live="polite"></span>
			<?php endif; ?>

			<?php
			$swatches_html = ob_get_clean();

			// Keep the original <select> too — but visually hidden. WooCommerce's
			// add-to-cart flow reads its value, and our JS keeps it in sync.
			$select_html = '<div class="storzen-swatch-shadow-select">' . $html . '</div>';

			return $select_html . $swatches_html;
		}

		/**
		 * Resolve the swatch type to use for an attribute. When module setting is
		 * 'auto', we check the global attribute's wc_attribute_type meta (WooCommerce
		 * supports custom attribute types via add_filter). Fall back to 'button'
		 * which works without per-term meta.
		 *
		 * @param string $attribute
		 * @return string color|image|button|svg
		 */
		private function resolve_attribute_swatch_type( $attribute ) {
			$module_default = (string) $this->get( 'swatch_type', 'color' );

			if ( 'auto' !== $module_default ) {
				return in_array( $module_default, array( 'color', 'image', 'button', 'svg' ), true )
					? $module_default
					: 'button';
			}

			// Auto mode — derive from global attribute settings (color, image, etc).
			if ( taxonomy_exists( $attribute ) && function_exists( 'wc_get_attribute' ) ) {
				$taxonomy_obj = get_taxonomy( $attribute );
				if ( $taxonomy_obj && isset( $taxonomy_obj->attribute_id ) ) {
					$global_attr = wc_get_attribute( $taxonomy_obj->attribute_id );
					if ( $global_attr && ! empty( $global_attr->type ) ) {
						$type = strtolower( $global_attr->type );
						if ( in_array( $type, array( 'color', 'image', 'button' ), true ) ) {
							return $type;
						}
					}
				}
			}

			// Heuristic fallback: attributes named "color"/"colour" default to color.
			$slug = strtolower( $attribute );
			if ( false !== strpos( $slug, 'color' ) || false !== strpos( $slug, 'colour' ) ) {
				return 'color';
			}

			return 'button';
		}

		/**
		 * Build the per-option entries used by the swatch renderer.
		 *
		 * Each entry: ['value' => 'red', 'label' => 'Red', 'color' => '#...', 'image' => 'url', 'svg' => '<svg>...']
		 *
		 * @param string $attribute
		 * @param array  $options       Raw option values (term slugs for taxonomies, plain strings otherwise)
		 * @param string $type
		 * @param bool   $is_taxonomy
		 * @return array
		 */
		private function build_swatch_entries( $attribute, $options, $type, $is_taxonomy ) {
			$entries  = array();
			$img_size = (string) $this->get( 'attribute_image_size', 'thumbnail' );

			foreach ( $options as $value ) {
				if ( $is_taxonomy ) {
					$term = get_term_by( 'slug', $value, $attribute );
					if ( ! $term ) {
						continue;
					}
					$label = $term->name;

					$entry = array(
						'value' => $value,
						'label' => $label,
					);

					// Read per-term meta. These keys are namespaced; missing meta
					// gracefully falls back to button rendering downstream.
					$entry['color'] = (string) get_term_meta( $term->term_id, '_storzen_swatch_color', true );

					$image_id = (int) get_term_meta( $term->term_id, '_storzen_swatch_image', true );
					if ( $image_id ) {
						$src = wp_get_attachment_image_url( $image_id, $img_size );
						if ( $src ) {
							$entry['image'] = $src;
						}
					}

					if ( 'svg' === $type && $this->get( 'pro_custom_shapes', false ) ) {
						$entry['svg'] = (string) get_term_meta( $term->term_id, '_storzen_swatch_svg', true );
					}
				} else {
					// Custom (non-taxonomy) attribute — value IS the label.
					$entry = array(
						'value' => $value,
						'label' => $value,
					);
				}

				$entries[] = $entry;
			}

			return $entries;
		}

		/**
		 * Render the inner visual for a single swatch. Always wrapped in the
		 * outer <span> by the caller; this returns only the inner markup.
		 *
		 * @param array  $entry
		 * @param string $type
		 * @return string
		 */
		private function render_swatch_visual( $entry, $type ) {
			switch ( $type ) {
				case 'color':
					$color = ! empty( $entry['color'] ) ? $this->sanitize_color( $entry['color'], '#cccccc' ) : '#cccccc';
					return sprintf(
						'<span class="storzen-swatch__color" style="background-color:%s"></span>',
						esc_attr( $color )
					);

				case 'image':
					if ( ! empty( $entry['image'] ) ) {
						return sprintf(
							'<img class="storzen-swatch__img" src="%s" alt="%s" loading="lazy">',
							esc_url( $entry['image'] ),
							esc_attr( $entry['label'] )
						);
					}
					// Image type with no image → fall through to button-style label.
					// Intentional fall-through.

				case 'svg':
					if ( ! empty( $entry['svg'] ) ) {
						// SVG is admin-supplied content stored in term meta. Use wp_kses
						// with the standard SVG allowlist so script tags are stripped.
						return wp_kses( $entry['svg'], $this->svg_kses_allowlist() );
					}
					// Fall through.

				case 'button':
				default:
					return sprintf(
						'<span class="storzen-swatch__label">%s</span>',
						esc_html( $entry['label'] )
					);
			}
		}

		/**
		 * Allowed SVG tags + attributes for term-stored SVG swatches.
		 * Conservative — only what's needed for simple decorative shapes.
		 *
		 * @return array
		 */
		private function svg_kses_allowlist() {
			$attr = array(
				'xmlns'       => true,
				'viewbox'     => true,
				'width'       => true,
				'height'      => true,
				'fill'        => true,
				'stroke'      => true,
				'stroke-width' => true,
				'd'           => true,
				'class'       => true,
			);
			return array(
				'svg'      => $attr,
				'g'        => $attr,
				'path'     => $attr,
				'circle'   => array_merge( $attr, array( 'cx' => true, 'cy' => true, 'r' => true ) ),
				'rect'     => array_merge( $attr, array( 'x' => true, 'y' => true, 'rx' => true, 'ry' => true ) ),
				'polygon'  => array_merge( $attr, array( 'points' => true ) ),
				'polyline' => array_merge( $attr, array( 'points' => true ) ),
				'line'     => array_merge( $attr, array( 'x1' => true, 'y1' => true, 'x2' => true, 'y2' => true ) ),
				'ellipse'  => array_merge( $attr, array( 'cx' => true, 'cy' => true, 'rx' => true, 'ry' => true ) ),
				'title'    => array(),
			);
		}

		/**
		 * Find attribute values that are sold out (every variation that uses them
		 * is out of stock). Used to dim/cross/hide those swatches.
		 *
		 * @param WC_Product $product
		 * @param string     $attribute
		 * @return array     Attribute values that are unavailable.
		 */
		private function find_disabled_attribute_values( $product, $attribute ) {
			$disabled = array();
			if ( ! $product instanceof WC_Product_Variable ) {
				return $disabled;
			}

			$available = $product->get_available_variations();
			$attribute_key = 'attribute_' . sanitize_title( $attribute );

			// Group: value => has_any_in_stock
			$value_stock = array();
			foreach ( $available as $var ) {
				if ( ! isset( $var['attributes'][ $attribute_key ] ) ) {
					continue;
				}
				$val      = (string) $var['attributes'][ $attribute_key ];
				$in_stock = ! empty( $var['is_in_stock'] );
				if ( ! isset( $value_stock[ $val ] ) ) {
					$value_stock[ $val ] = false;
				}
				if ( $in_stock ) {
					$value_stock[ $val ] = true;
				}
			}

			foreach ( $value_stock as $val => $has_stock ) {
				if ( ! $has_stock && '' !== $val ) {
					$disabled[] = $val;
				}
			}

			return $disabled;
		}

		// ─────────────────────────────────────────────────────────────────
		// Shop / category card swatches
		// ─────────────────────────────────────────────────────────────────

		/**
		 * Render a compact swatch row under each shop-loop product title.
		 * Only shows the FIRST variation attribute (typically Color) to keep
		 * the card uncluttered.
		 */
		public function render_shop_swatches( $product_arg = null ) {
			$product = ( $product_arg instanceof WC_Product ) ? $product_arg : ( $GLOBALS['product'] ?? null );
			if ( ! $product instanceof WC_Product_Variable ) {
				return;
			}

			$attributes = $product->get_variation_attributes();
			if ( empty( $attributes ) ) {
				return;
			}

			// Pick the first attribute only.
			$attribute = key( $attributes );
			$options   = reset( $attributes );
			if ( empty( $options ) ) {
				return;
			}

			// Re-use the swatch renderer with a minimal args bundle.
			$selected = $product->get_variation_default_attribute( $attribute );

			$args = array(
				'options'   => $options,
				'attribute' => $attribute,
				'product'   => $product,
				'selected'  => $selected,
			);

			echo '<div class="storzen-swatches-shop" data-product_id="' . esc_attr( $product->get_id() ) . '">';
			echo $this->render_swatches_html( '', $args ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- sub-method escapes
			echo '</div>';
		}

		// ─────────────────────────────────────────────────────────────────
		// Admin — per-term swatch meta UI
		// ─────────────────────────────────────────────────────────────────

		/**
		 * Hook the swatch meta fields into every product attribute taxonomy.
		 * Both the "Add new term" and "Edit term" screens get the UI.
		 */
		private function register_term_meta_ui() {
			$attribute_taxonomies = function_exists( 'wc_get_attribute_taxonomies' )
				? wc_get_attribute_taxonomies()
				: array();

			foreach ( $attribute_taxonomies as $tax ) {
				$taxonomy = wc_attribute_taxonomy_name( $tax->attribute_name );
				add_action( "{$taxonomy}_add_form_fields",  array( $this, 'term_add_form_fields' ), 10, 1 );
				add_action( "{$taxonomy}_edit_form_fields", array( $this, 'term_edit_form_fields' ), 10, 2 );
				add_action( "edited_{$taxonomy}",           array( $this, 'save_term_meta' ),       10, 2 );
				add_action( "create_{$taxonomy}",           array( $this, 'save_term_meta' ),       10, 2 );
			}
		}

		/**
		 * "Add new term" screen fields.
		 *
		 * @param string $taxonomy
		 */
		public function term_add_form_fields( $taxonomy ) {
			?>
			<div class="form-field">
				<label for="storzen-swatch-color"><?php esc_html_e( 'Storzen swatch color', 'storzen' ); ?></label>
				<input type="text" name="storzen_swatch_color" id="storzen-swatch-color" value="" placeholder="#RRGGBB" class="storzen-swatch-color-field">
				<p class="description"><?php esc_html_e( 'Used when this attribute renders as a color swatch.', 'storzen' ); ?></p>
			</div>
			<div class="form-field">
				<label><?php esc_html_e( 'Storzen swatch image', 'storzen' ); ?></label>
				<input type="hidden" name="storzen_swatch_image" id="storzen-swatch-image" value="">
				<button type="button" class="button storzen-swatch-image-upload"><?php esc_html_e( 'Choose image', 'storzen' ); ?></button>
				<button type="button" class="button storzen-swatch-image-remove" style="display:none"><?php esc_html_e( 'Remove', 'storzen' ); ?></button>
				<div class="storzen-swatch-image-preview"></div>
				<p class="description"><?php esc_html_e( 'Used when this attribute renders as an image swatch.', 'storzen' ); ?></p>
			</div>
			<?php
		}

		/**
		 * "Edit term" screen fields.
		 *
		 * @param WP_Term $term
		 * @param string  $taxonomy
		 */
		public function term_edit_form_fields( $term, $taxonomy ) {
			$color      = (string) get_term_meta( $term->term_id, '_storzen_swatch_color', true );
			$image_id   = (int) get_term_meta( $term->term_id, '_storzen_swatch_image', true );
			$image_url  = $image_id ? wp_get_attachment_image_url( $image_id, 'thumbnail' ) : '';
			$is_pro     = defined( 'STORZEN_IS_PRO' ) && 'yes' === STORZEN_IS_PRO;
			$svg        = $is_pro ? (string) get_term_meta( $term->term_id, '_storzen_swatch_svg', true ) : '';
			?>
			<tr class="form-field">
				<th scope="row"><label for="storzen-swatch-color"><?php esc_html_e( 'Storzen swatch color', 'storzen' ); ?></label></th>
				<td>
					<input type="text" name="storzen_swatch_color" id="storzen-swatch-color" value="<?php echo esc_attr( $color ); ?>" placeholder="#RRGGBB" class="storzen-swatch-color-field">
					<p class="description"><?php esc_html_e( 'Hex color used when this attribute renders as a color swatch.', 'storzen' ); ?></p>
				</td>
			</tr>
			<tr class="form-field">
				<th scope="row"><label><?php esc_html_e( 'Storzen swatch image', 'storzen' ); ?></label></th>
				<td>
					<input type="hidden" name="storzen_swatch_image" id="storzen-swatch-image" value="<?php echo esc_attr( $image_id ); ?>">
					<div class="storzen-swatch-image-preview">
						<?php if ( $image_url ) : ?>
							<img src="<?php echo esc_url( $image_url ); ?>" alt="" style="max-width:80px;height:auto;border-radius:6px;border:1px solid #ddd;display:block;margin-bottom:8px">
						<?php endif; ?>
					</div>
					<button type="button" class="button storzen-swatch-image-upload">
						<?php echo $image_url ? esc_html__( 'Replace image', 'storzen' ) : esc_html__( 'Choose image', 'storzen' ); ?>
					</button>
					<button type="button" class="button storzen-swatch-image-remove" <?php echo $image_url ? '' : 'style="display:none"'; ?>>
						<?php esc_html_e( 'Remove', 'storzen' ); ?>
					</button>
					<p class="description"><?php esc_html_e( 'Used when this attribute renders as an image swatch.', 'storzen' ); ?></p>
				</td>
			</tr>
			<?php if ( $is_pro ) : ?>
				<tr class="form-field">
					<th scope="row"><label for="storzen-swatch-svg"><?php esc_html_e( 'Custom SVG shape (Pro)', 'storzen' ); ?></label></th>
					<td>
						<textarea name="storzen_swatch_svg" id="storzen-swatch-svg" rows="4" cols="50" placeholder="&lt;svg ...&gt;...&lt;/svg&gt;"><?php echo esc_textarea( $svg ); ?></textarea>
						<p class="description"><?php esc_html_e( 'Paste an inline SVG. Used when "Allow Custom SVG Shapes" is enabled in the module settings. Scripts and event handlers are stripped automatically.', 'storzen' ); ?></p>
					</td>
				</tr>
			<?php endif; ?>
			<?php
		}

		/**
		 * Persist swatch meta when an attribute term is saved.
		 * Capability + nonce checks: WordPress already verifies these on
		 * the edit-tags screen before firing the create_* / edited_* actions.
		 *
		 * @param int $term_id
		 */
		public function save_term_meta( $term_id ) {
			if ( ! current_user_can( 'manage_product_terms' ) ) {
				return;
			}

			// Nonce is verified by WordPress core for the term edit/create flow
			// (edited_terms / created_term), and an explicit capability check is
			// performed above, so these reads do not require a separate nonce.
			// phpcs:disable WordPress.Security.NonceVerification.Missing

			// Color
			if ( isset( $_POST['storzen_swatch_color'] ) ) {
				$color = sanitize_hex_color( wp_unslash( $_POST['storzen_swatch_color'] ) );
				if ( $color ) {
					update_term_meta( $term_id, '_storzen_swatch_color', $color );
				} else {
					delete_term_meta( $term_id, '_storzen_swatch_color' );
				}
			}

			// Image
			if ( isset( $_POST['storzen_swatch_image'] ) ) {
				$image_id = absint( wp_unslash( $_POST['storzen_swatch_image'] ) );
				if ( $image_id ) {
					update_term_meta( $term_id, '_storzen_swatch_image', $image_id );
				} else {
					delete_term_meta( $term_id, '_storzen_swatch_image' );
				}
			}

			// SVG (Pro)
			$is_pro = defined( 'STORZEN_IS_PRO' ) && 'yes' === STORZEN_IS_PRO;
			if ( $is_pro && isset( $_POST['storzen_swatch_svg'] ) ) {
				// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized on the next line via wp_kses() with an explicit SVG allowlist.
				$svg_raw = wp_unslash( $_POST['storzen_swatch_svg'] );
				$svg     = wp_kses( $svg_raw, $this->svg_kses_allowlist() );
				if ( $svg ) {
					update_term_meta( $term_id, '_storzen_swatch_svg', $svg );
				} else {
					delete_term_meta( $term_id, '_storzen_swatch_svg' );
				}
			}
			// phpcs:enable WordPress.Security.NonceVerification.Missing
		}

		/**
		 * Admin: enqueue media uploader + color picker on attribute term screens.
		 *
		 * @param string $hook
		 */
		public function enqueue_admin_assets( $hook ) {
			// edit-tags.php?taxonomy=pa_color (et al). We don't gate on the exact
			// taxonomy because the JS handlers are idempotent and only fire when
			// the fields exist.
			if ( 'edit-tags.php' !== $hook && 'term.php' !== $hook ) {
				return;
			}

			$screen = get_current_screen();
			if ( ! $screen || empty( $screen->taxonomy ) ) {
				return;
			}
			// Only on WooCommerce attribute taxonomies.
			if ( 0 !== strpos( $screen->taxonomy, 'pa_' ) ) {
				return;
			}

			$base_url = defined( 'STORZEN_ROOT_URL' ) ? STORZEN_ROOT_URL : plugin_dir_url( dirname( dirname( __FILE__ ) ) );
			$version  = defined( 'STORZEN_VERSION' ) ? STORZEN_VERSION : '1.0.0';

			wp_enqueue_media();
			wp_enqueue_style( 'wp-color-picker' );
			wp_enqueue_script(
				'storzen-swatch-admin',
				$base_url . 'assets/js/modules/variation-swatches-admin.js',
				array( 'jquery', 'wp-color-picker' ),
				$version,
				true
			);
			wp_localize_script(
				'storzen-swatch-admin',
				'StorzenSwatchAdminI18n',
				array(
					'chooseImage' => __( 'Choose swatch image', 'storzen' ),
					'useImage'    => __( 'Use this image', 'storzen' ),
				)
			);
		}

	} // end class

} // end class_exists
