<?php
/**
 * Swatch Attribute Types module.
 *
 * @package Kramar
 * @since   1.0.0
 */

declare( strict_types=1 );

namespace Kramar\Modules\Products;

// Prevent direct access.
defined( 'ABSPATH' ) || exit;

/**
 * Swatch Attribute Types module.
 *
 * Replaces plain <select> dropdowns with color, image, or button swatches on
 * single product pages for WooCommerce attributes that have a swatch type set.
 *
 * @since 1.0.0
 */
final class Swatches extends \Kramar\Module {

	/**
	 * {@inheritDoc}
	 */
	public function get_slug(): string {
		return 'swatches';
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_category(): string {
		return 'products';
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_name(): string {
		return __( 'Swatch Attribute Types', 'kramar' );
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_description(): string {
		return __( 'Replace plain dropdowns with color, image, or button swatches for product variation attributes on single product pages.', 'kramar' );
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_default_settings(): array {
		return [
			'enabled'      => false,
			'swatch_size'  => 32,
			'show_tooltip' => true,
		];
	}

	/**
	 * {@inheritDoc}
	 */
	public function sanitize_settings( array $input ): array {
		$new_size = max( 16, min( 96, (int) ( $input['swatch_size'] ?? 32 ) ) );
		$old_size = (int) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'swatch_size' );

		if ( $new_size !== $old_size ) {
			$this->regenerate_swatch_thumbnails( $new_size, $old_size );
		}

		return [
			'enabled'      => ! empty( $input['enabled'] ),
			'swatch_size'  => $new_size,
			'show_tooltip' => ! empty( $input['show_tooltip'] ),
		];
	}

	/**
	 * {@inheritDoc}
	 */
	private const IMAGE_SIZE = 'kramar_swatch';

	protected function run(): void {
		$size = (int) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'swatch_size' );
		$px   = max( 16, $size * 2 ); // 2x for retina
		add_image_size( self::IMAGE_SIZE, $px, $px, true );

		add_filter( 'product_attributes_type_selector', [ $this, 'register_swatch_types' ] );
		add_action( 'admin_init', [ $this, 'register_term_meta_hooks' ] );
		add_action( 'woocommerce_product_option_terms', [ $this, 'render_product_option_terms' ], 10, 3 );
		add_filter( 'woocommerce_dropdown_variation_attribute_options_html', [ $this, 'render_swatches' ], 10, 2 );
		add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_frontend_assets' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_term_media' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_product_admin_assets' ] );
		add_action( 'woocommerce_variation_header', [ $this, 'render_variation_swatch_header' ], 10, 2 );
		add_action( 'woocommerce_product_after_variable_attributes', [ $this, 'render_variation_swatch_panel' ], 5, 3 );
	}

	// -------------------------------------------------------------------------
	// Attribute Type Registration
	// -------------------------------------------------------------------------

	/**
	 * Add swatch types to the attribute type selector.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, string> $types Existing attribute types.
	 * @return array<string, string>
	 */
	public function register_swatch_types( array $types ): array {
		$types['color']  = __( 'Color swatch', 'kramar' );
		$types['image']  = __( 'Image swatch', 'kramar' );
		$types['button'] = __( 'Button/label swatch', 'kramar' );
		return $types;
	}

	// -------------------------------------------------------------------------
	// Product Edit: Attribute Term Selector
	// -------------------------------------------------------------------------

	/**
	 * Render the term selector on the product edit Attributes tab for swatch types.
	 *
	 * WooCommerce only renders its select2 term picker for the built-in 'select'
	 * type. This replicates the exact same markup so WC's JS handles it natively.
	 *
	 * @since 1.0.0
	 *
	 * @param object $attribute_taxonomy Attribute taxonomy object.
	 * @param int    $i                  Attribute position index.
	 * @param object $attribute          Product attribute object.
	 */
	public function render_product_option_terms( object $attribute_taxonomy, int $i, object $attribute ): void {
		if ( ! in_array( $attribute_taxonomy->attribute_type, [ 'color', 'image', 'button' ], true ) ) {
			return;
		}

		$attribute_orderby = ! empty( $attribute_taxonomy->attribute_orderby ) ? $attribute_taxonomy->attribute_orderby : 'name';
		// Term-picker fetch limit for the select2 control. Uses our own prefixed
		// filter (WC's default is 50) so it is overridable without firing another
		// plugin's internal admin hook from our code.
		$term_limit        = absint( apply_filters( 'kramar_swatch_term_limit', 50 ) );
		?>
		<select multiple="multiple"
			data-minimum_input_length="0"
			data-limit="<?php echo esc_attr( (string) $term_limit ); ?>"
			data-return_id="id"
			data-placeholder="<?php esc_attr_e( 'Select values', 'kramar' ); ?>"
			data-orderby="<?php echo esc_attr( $attribute_orderby ); ?>"
			class="multiselect attribute_values wc-taxonomy-term-search"
			name="attribute_values[<?php echo esc_attr( (string) $i ); ?>][]"
			data-taxonomy="<?php echo esc_attr( $attribute->get_taxonomy() ); ?>"
		>
			<?php
			$selected_terms = $attribute->get_terms();
			if ( $selected_terms ) {
				foreach ( $selected_terms as $selected_term ) {
					// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce hook
					echo '<option value="' . esc_attr( (string) $selected_term->term_id ) . '" selected="selected">' . esc_html( apply_filters( 'woocommerce_product_attribute_term_name', $selected_term->name, $selected_term ) ) . '</option>';
				}
			}
			?>
		</select>
		<button class="button plus select_all_attributes"><?php esc_html_e( 'Select all', 'kramar' ); ?></button>
		<button class="button minus select_no_attributes"><?php esc_html_e( 'Select none', 'kramar' ); ?></button>
		<button class="button fr plus add_new_attribute"><?php esc_html_e( 'Create value', 'kramar' ); ?></button>
		<?php
	}

	// -------------------------------------------------------------------------
	// Term Meta: Registration
	// -------------------------------------------------------------------------

	/**
	 * Register term meta field hooks for all swatch-type attribute taxonomies.
	 *
	 * Also registers list table columns to show swatch previews.
	 *
	 * @since 1.0.0
	 */
	public function register_term_meta_hooks(): void {
		$attribute_taxonomies = wc_get_attribute_taxonomies();
		if ( empty( $attribute_taxonomies ) ) {
			return;
		}

		foreach ( $attribute_taxonomies as $attr ) {
			$type     = $attr->attribute_type;
			$taxonomy = wc_attribute_taxonomy_name( $attr->attribute_name );

			if ( 'color' === $type ) {
				add_action( "{$taxonomy}_add_form_fields", [ $this, 'add_color_field' ] );
				add_action( "{$taxonomy}_edit_form_fields", [ $this, 'edit_color_field' ], 10, 2 );
				add_action( "created_{$taxonomy}", [ $this, 'save_color_meta' ] );
				add_action( "edited_{$taxonomy}", [ $this, 'save_color_meta' ] );

				add_filter( "manage_edit-{$taxonomy}_columns", [ $this, 'add_swatch_column' ] );
				add_filter( "manage_{$taxonomy}_custom_column", [ $this, 'render_color_column' ], 10, 3 );
			} elseif ( 'image' === $type ) {
				add_action( "{$taxonomy}_add_form_fields", [ $this, 'add_image_field' ] );
				add_action( "{$taxonomy}_edit_form_fields", [ $this, 'edit_image_field' ], 10, 2 );
				add_action( "created_{$taxonomy}", [ $this, 'save_image_meta' ] );
				add_action( "edited_{$taxonomy}", [ $this, 'save_image_meta' ] );

				add_filter( "manage_edit-{$taxonomy}_columns", [ $this, 'add_swatch_column' ] );
				add_filter( "manage_{$taxonomy}_custom_column", [ $this, 'render_image_column' ], 10, 3 );
			} elseif ( 'button' === $type ) {
				add_filter( "manage_edit-{$taxonomy}_columns", [ $this, 'add_swatch_column' ] );
				add_filter( "manage_{$taxonomy}_custom_column", [ $this, 'render_button_column' ], 10, 3 );
			}
		}
	}

	// -------------------------------------------------------------------------
	// Term Meta: Color Fields
	// -------------------------------------------------------------------------

	/**
	 * Render color picker in the "add term" form.
	 *
	 * @since 1.0.0
	 *
	 * @param string $taxonomy Current taxonomy name.
	 */
	public function add_color_field( string $taxonomy ): void {
		?>
		<div class="form-field">
			<label for="kramar-swatch-color"><?php esc_html_e( 'Swatch Color', 'kramar' ); ?></label>
			<div style="display:flex;align-items:center;gap:8px;">
				<input type="color" id="kramar-swatch-color" name="kramar_swatch_color" value="#ffffff" class="kramar-color-picker" />
				<input type="text" id="kramar-swatch-color-hex" value="#ffffff" maxlength="7" style="width:90px;font-family:monospace;" class="kramar-color-hex" />
			</div>
			<p class="description"><?php esc_html_e( 'Pick the color this term represents.', 'kramar' ); ?></p>
		</div>
		<?php
	}

	/**
	 * Render color picker in the "edit term" form.
	 *
	 * @since 1.0.0
	 *
	 * @param \WP_Term $term     Term object.
	 * @param string   $taxonomy Current taxonomy name.
	 */
	public function edit_color_field( \WP_Term $term, string $taxonomy ): void {
		$color = (string) get_term_meta( $term->term_id, 'kramar_swatch_color', true );
		if ( '' === $color ) {
			$color = '#ffffff';
		}
		?>
		<tr class="form-field">
			<th scope="row"><label for="kramar-swatch-color"><?php esc_html_e( 'Swatch Color', 'kramar' ); ?></label></th>
			<td>
				<div style="display:flex;align-items:center;gap:8px;">
					<input type="color" id="kramar-swatch-color" name="kramar_swatch_color" value="<?php echo esc_attr( $color ); ?>" class="kramar-color-picker" />
					<input type="text" id="kramar-swatch-color-hex" value="<?php echo esc_attr( $color ); ?>" maxlength="7" style="width:90px;font-family:monospace;" class="kramar-color-hex" />
				</div>
				<p class="description"><?php esc_html_e( 'Pick the color this term represents.', 'kramar' ); ?></p>
			</td>
		</tr>
		<?php
	}

	/**
	 * Save color term meta.
	 *
	 * @since 1.0.0
	 *
	 * @param int $term_id Term ID.
	 */
	public function save_color_meta( int $term_id ): void {
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce + capability verified in verify_term_save() (add-tag / update-tag_{id}).
		if ( ! $this->verify_term_save( $term_id ) || ! isset( $_POST['kramar_swatch_color'] ) ) {
			return;
		}
		$color = sanitize_hex_color( wp_unslash( (string) $_POST['kramar_swatch_color'] ) );
		// phpcs:enable WordPress.Security.NonceVerification.Missing
		if ( $color ) {
			update_term_meta( $term_id, 'kramar_swatch_color', $color );
		}
	}

	/**
	 * Verify the request saving a swatch term meta value is a legitimate,
	 * authorised term add/edit — capability + core term nonce.
	 *
	 * The `created_/edited_{$taxonomy}` actions can fire from any
	 * `wp_insert_term()`/`wp_update_term()` call, so the callback must not
	 * trust `$_POST` on its own. The "Add term" (AJAX) flow sends the
	 * `add-tag` nonce; the "Edit term" flow sends the `update-tag_{id}` nonce.
	 *
	 * @since 1.0.0
	 *
	 * @param int $term_id Term being saved.
	 * @return bool True when the request is a verified admin term save.
	 */
	private function verify_term_save( int $term_id ): bool {
		if ( ! current_user_can( 'manage_woocommerce' ) ) {
			return false;
		}
		if ( isset( $_POST['_wpnonce_add-tag'] ) ) {
			return (bool) wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce_add-tag'] ) ), 'add-tag' );
		}
		if ( isset( $_POST['_wpnonce'] ) ) {
			return (bool) wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ), 'update-tag_' . $term_id );
		}
		return false;
	}

	// -------------------------------------------------------------------------
	// Term Meta: Image Fields
	// -------------------------------------------------------------------------

	/**
	 * Render image upload field in the "add term" form.
	 *
	 * @since 1.0.0
	 *
	 * @param string $taxonomy Current taxonomy name.
	 */
	public function add_image_field( string $taxonomy ): void {
		?>
		<div class="form-field">
			<label><?php esc_html_e( 'Swatch Image', 'kramar' ); ?></label>
			<div class="kramar-swatch-image-wrap">
				<img class="kramar-swatch-image-preview" src="" style="max-width:60px;max-height:60px;display:none;" alt="" />
				<input type="hidden" class="kramar-swatch-image-id" name="kramar_swatch_image" value="" />
				<button type="button" class="button kramar-upload-swatch-image"><?php esc_html_e( 'Upload image', 'kramar' ); ?></button>
				<button type="button" class="button kramar-remove-swatch-image" style="display:none;"><?php esc_html_e( 'Remove', 'kramar' ); ?></button>
			</div>
			<p class="description"><?php esc_html_e( 'Select an image for this swatch term.', 'kramar' ); ?></p>
		</div>
		<?php
	}

	/**
	 * Render image upload field in the "edit term" form.
	 *
	 * @since 1.0.0
	 *
	 * @param \WP_Term $term     Term object.
	 * @param string   $taxonomy Current taxonomy name.
	 */
	public function edit_image_field( \WP_Term $term, string $taxonomy ): void {
		$image_id  = (int) get_term_meta( $term->term_id, 'kramar_swatch_image', true );
		$image_url = $this->get_swatch_image_url( $image_id );
		?>
		<tr class="form-field">
			<th scope="row"><label><?php esc_html_e( 'Swatch Image', 'kramar' ); ?></label></th>
			<td>
				<div class="kramar-swatch-image-wrap">
					<img class="kramar-swatch-image-preview" src="<?php echo esc_url( (string) $image_url ); ?>" style="max-width:60px;max-height:60px;<?php echo $image_url ? '' : 'display:none;'; ?>" alt="" />
					<input type="hidden" class="kramar-swatch-image-id" name="kramar_swatch_image" value="<?php echo esc_attr( (string) $image_id ); ?>" />
					<button type="button" class="button kramar-upload-swatch-image"><?php esc_html_e( 'Upload image', 'kramar' ); ?></button>
					<button type="button" class="button kramar-remove-swatch-image"<?php echo $image_url ? '' : ' style="display:none;"'; ?>><?php esc_html_e( 'Remove', 'kramar' ); ?></button>
				</div>
				<p class="description"><?php esc_html_e( 'Select an image for this swatch term.', 'kramar' ); ?></p>
			</td>
		</tr>
		<?php
	}

	/**
	 * Save image term meta.
	 *
	 * @since 1.0.0
	 *
	 * @param int $term_id Term ID.
	 */
	public function save_image_meta( int $term_id ): void {
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce + capability verified in verify_term_save() (add-tag / update-tag_{id}).
		if ( ! $this->verify_term_save( $term_id ) || ! isset( $_POST['kramar_swatch_image'] ) ) {
			return;
		}
		$image_id = absint( wp_unslash( $_POST['kramar_swatch_image'] ) );
		// phpcs:enable WordPress.Security.NonceVerification.Missing
		if ( $image_id > 0 ) {
			update_term_meta( $term_id, 'kramar_swatch_image', $image_id );
			$this->ensure_swatch_size_exists( $image_id );
		} else {
			delete_term_meta( $term_id, 'kramar_swatch_image' );
		}
	}

	// -------------------------------------------------------------------------
	// Term List Table: Swatch Preview Columns
	// -------------------------------------------------------------------------

	/**
	 * Add a "Swatch" column to the attribute term list table.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, string> $columns Existing columns.
	 * @return array<string, string>
	 */
	public function add_swatch_column( array $columns ): array {
		$new = [];
		foreach ( $columns as $key => $label ) {
			if ( 'name' === $key ) {
				$new[ $key ]         = $label;
				$new['kramar_swatch'] = __( 'Swatch', 'kramar' );
			} else {
				$new[ $key ] = $label;
			}
		}
		return $new;
	}

	/**
	 * Render color swatch preview in the term list column.
	 *
	 * @since 1.0.0
	 *
	 * @param string $content     Existing column content.
	 * @param string $column_name Column name.
	 * @param int    $term_id     Term ID.
	 * @return string
	 */
	public function render_color_column( string $content, string $column_name, int $term_id ): string {
		if ( 'kramar_swatch' !== $column_name ) {
			return $content;
		}
		$color = (string) get_term_meta( $term_id, 'kramar_swatch_color', true );
		if ( '' === $color ) {
			// L18: distinguish "no colour set" from a term whose colour genuinely
			// is grey, and hint that the front-end swatch will fall back to grey.
			return sprintf(
				'<span style="display:inline-block;width:28px;height:28px;border-radius:50%%;background:repeating-linear-gradient(45deg,#eee,#eee 4px,#ccc 4px,#ccc 8px);border:2px dashed #bbb;vertical-align:middle;" title="%s"></span>',
				esc_attr__( 'No swatch colour set — edit this term to pick one (front-end falls back to grey).', 'kramar' )
			);
		}
		return sprintf(
			'<span style="display:inline-block;width:28px;height:28px;border-radius:50%%;background:%s;border:2px solid rgba(0,0,0,.1);vertical-align:middle;" title="%s"></span>',
			esc_attr( $color ),
			esc_attr( $color )
		);
	}

	/**
	 * Render image swatch preview in the term list column.
	 *
	 * @since 1.0.0
	 *
	 * @param string $content     Existing column content.
	 * @param string $column_name Column name.
	 * @param int    $term_id     Term ID.
	 * @return string
	 */
	public function render_image_column( string $content, string $column_name, int $term_id ): string {
		if ( 'kramar_swatch' !== $column_name ) {
			return $content;
		}
		$image_id  = (int) get_term_meta( $term_id, 'kramar_swatch_image', true );
		$image_url = $this->get_swatch_image_url( $image_id );
		if ( '' === $image_url ) {
			return '<span style="color:#999;">&mdash;</span>';
		}
		return sprintf(
			'<img src="%s" alt="" style="width:28px;height:28px;object-fit:cover;border-radius:4px;border:1px solid rgba(0,0,0,.1);vertical-align:middle;" />',
			esc_url( $image_url )
		);
	}

	/**
	 * Render button swatch preview in the term list column.
	 *
	 * @since 1.0.0
	 *
	 * @param string $content     Existing column content.
	 * @param string $column_name Column name.
	 * @param int    $term_id     Term ID.
	 * @return string
	 */
	public function render_button_column( string $content, string $column_name, int $term_id ): string {
		if ( 'kramar_swatch' !== $column_name ) {
			return $content;
		}
		$term = get_term( $term_id );
		if ( ! $term instanceof \WP_Term ) {
			return $content;
		}
		return sprintf(
			'<span style="display:inline-block;padding:2px 10px;border:1px solid #ddd;border-radius:3px;font-size:12px;background:#f9f9f9;">%s</span>',
			esc_html( $term->name )
		);
	}

	// -------------------------------------------------------------------------
	// Frontend: Swatch Rendering
	// -------------------------------------------------------------------------

	/**
	 * Replace the variation <select> with swatch HTML for swatch-type attributes.
	 *
	 * @since 1.0.0
	 *
	 * @param string               $html Default select HTML.
	 * @param array<string, mixed> $args Dropdown args from WooCommerce.
	 * @return string
	 */
	public function render_swatches( string $html, array $args ): string {
		$attribute = (string) ( $args['attribute'] ?? '' );
		$type      = $this->get_attribute_type( $attribute );

		if ( ! in_array( $type, [ 'color', 'image', 'button' ], true ) ) {
			return $html;
		}

		$options  = $args['options'] ?? [];
		$selected = (string) ( $args['selected'] ?? '' );
		$size     = (int) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'swatch_size' );
		$tooltip  = (bool) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'show_tooltip' );

		$group_label = wc_attribute_label( $attribute );
		$swatches    = '<div class="kramar-swatches" data-attribute="' . esc_attr( $attribute ) . '" role="radiogroup" aria-label="' . esc_attr( $group_label ) . '">';

		foreach ( $options as $option ) {
			$term = get_term_by( 'slug', $option, $attribute );
			if ( ! $term instanceof \WP_Term ) {
				continue;
			}

			$is_active  = $selected === $option;
			$active     = $is_active ? ' kramar-swatch--active' : '';
			$title_attr = $tooltip ? ' title="' . esc_attr( $term->name ) . '"' : '';
			// Accessibility: expose each swatch as a focusable radio. aria-label is
			// always set (term name) so screen-reader/keyboard users can pick a
			// variation even when the browser tooltip is off. Roving tabindex: the
			// active swatch (or, if none, the first — handled in JS) is tabbable.
			$a11y = sprintf(
				' role="radio" tabindex="%s" aria-checked="%s" aria-label="%s"',
				$is_active ? '0' : '-1',
				$is_active ? 'true' : 'false',
				esc_attr( $term->name )
			);

			switch ( $type ) {
				case 'color':
					$color     = (string) get_term_meta( $term->term_id, 'kramar_swatch_color', true );
					$color     = '' !== $color ? $color : '#cccccc';
					$swatches .= sprintf(
						'<span class="kramar-swatch kramar-swatch--color%s" data-value="%s"%s%s style="background-color:%s;width:%dpx;height:%dpx;"></span>',
						$active,
						esc_attr( $option ),
						$title_attr,
						$a11y,
						esc_attr( $color ),
						$size,
						$size
					);
					break;

				case 'image':
					$image_id  = (int) get_term_meta( $term->term_id, 'kramar_swatch_image', true );
					$image_url = $this->get_swatch_image_url( $image_id );
					$swatches .= sprintf(
						'<span class="kramar-swatch kramar-swatch--image%s" data-value="%s"%s%s style="width:%dpx;height:%dpx;"><img src="%s" alt="%s"></span>',
						$active,
						esc_attr( $option ),
						$title_attr,
						$a11y,
						$size,
						$size,
						esc_url( $image_url ),
						esc_attr( $term->name )
					);
					break;

				case 'button':
					$swatches .= sprintf(
						'<span class="kramar-swatch kramar-swatch--button%s" data-value="%s"%s%s>%s</span>',
						$active,
						esc_attr( $option ),
						$title_attr,
						$a11y,
						esc_html( $term->name )
					);
					break;
			}
		}

		$swatches .= '</div>';

		// Keep the <select> so WooCommerce variation JS keeps working. The swatches
		// above are the accessible control (role="radio"), so the native select is
		// hidden from AT to avoid a duplicate announcement, but must stay in the
		// accessibility tree's absence WITHOUT display:none stripping its value —
		// WC reads/writes select.value, which still works while visually hidden.
		$hidden_select = str_replace(
			'<select',
			'<select style="display:none!important;position:absolute;" aria-hidden="true" tabindex="-1"',
			$html
		);

		return $swatches . $hidden_select;
	}

	/**
	 * Enqueue frontend CSS and JS on single product pages.
	 *
	 * @since 1.0.0
	 */
	public function enqueue_frontend_assets(): void {
		if ( ! is_product() ) {
			return;
		}

		$size = (int) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'swatch_size' );

		$css = '
.kramar-swatches{display:flex;gap:6px;flex-wrap:wrap;margin:8px 0;}
.kramar-swatch{display:inline-flex;align-items:center;justify-content:center;cursor:pointer;border:2px solid transparent;border-radius:4px;transition:border-color .2s,box-shadow .2s;box-sizing:border-box;}
.kramar-swatch--color{border-radius:50%;}
.kramar-swatch--image{overflow:hidden;}
.kramar-swatch--image img{width:100%;height:100%;object-fit:cover;display:block;}
.kramar-swatch--button{padding:4px 12px;border:1px solid #ddd;font-size:13px;}
.kramar-swatch--active{border-color:#333;box-shadow:0 0 0 1px #333;}
.kramar-swatch--disabled{opacity:.3;cursor:not-allowed;text-decoration:line-through;}
.kramar-swatch:focus{outline:none;}
.kramar-swatch:focus-visible{outline:2px solid #2271b1;outline-offset:2px;box-shadow:0 0 0 1px #2271b1;}
';

		wp_add_inline_style( 'woocommerce-general', $css );

		$js = '
(function(){
	function findSelect(container){
		var sel=container.nextElementSibling;
		while(sel&&sel.tagName!=="SELECT")sel=sel.nextElementSibling;
		return sel;
	}
	function selectSwatch(swatch){
		if(!swatch||swatch.classList.contains("kramar-swatch--disabled"))return;
		var container=swatch.closest(".kramar-swatches");
		if(!container)return;
		var sel=findSelect(container);
		container.querySelectorAll(".kramar-swatch").forEach(function(s){
			s.classList.remove("kramar-swatch--active");
			s.setAttribute("aria-checked","false");
			s.setAttribute("tabindex","-1");
		});
		swatch.classList.add("kramar-swatch--active");
		swatch.setAttribute("aria-checked","true");
		swatch.setAttribute("tabindex","0");
		if(sel){sel.value=swatch.dataset.value;sel.dispatchEvent(new Event("change",{bubbles:true}));}
	}

	document.addEventListener("click",function(e){
		var swatch=e.target.closest(".kramar-swatch");
		if(!swatch)return;
		selectSwatch(swatch);
	});

	// Keyboard: radiogroup semantics — arrows move + select, Space/Enter select.
	document.addEventListener("keydown",function(e){
		var swatch=e.target&&e.target.closest?e.target.closest(".kramar-swatch"):null;
		if(!swatch)return;
		var container=swatch.closest(".kramar-swatches");
		if(!container)return;
		var list=Array.prototype.slice.call(container.querySelectorAll(".kramar-swatch")).filter(function(s){return !s.classList.contains("kramar-swatch--disabled");});
		if(!list.length)return;
		var idx=list.indexOf(swatch);
		if(idx<0)idx=0;
		var key=e.key,next=null;
		if(key==="ArrowRight"||key==="ArrowDown"){next=list[(idx+1)%list.length];}
		else if(key==="ArrowLeft"||key==="ArrowUp"){next=list[(idx-1+list.length)%list.length];}
		else if(key==="Home"){next=list[0];}
		else if(key==="End"){next=list[list.length-1];}
		else if(key===" "||key==="Spacebar"||key==="Enter"){e.preventDefault();selectSwatch(swatch);return;}
		else{return;}
		if(next){e.preventDefault();selectSwatch(next);next.focus();}
	});

	// Clear the active highlight when the WooCommerce "Clear" link is used.
	document.addEventListener("click",function(e){
		var reset=e.target.closest(".reset_variations");
		if(!reset)return;
		var form=reset.closest(".variations_form")||document;
		form.querySelectorAll(".kramar-swatches").forEach(function(container){
			container.querySelectorAll(".kramar-swatch").forEach(function(s,i){
				s.classList.remove("kramar-swatch--active");
				s.setAttribute("aria-checked","false");
				s.setAttribute("tabindex",i===0?"0":"-1");
			});
		});
	});

	document.addEventListener("woocommerce_update_variation_values",function(){
		document.querySelectorAll(".kramar-swatches").forEach(function(container){
			var sel=findSelect(container);
			if(!sel)return;
			var opts=sel.querySelectorAll("option");
			container.querySelectorAll(".kramar-swatch").forEach(function(swatch){
				var opt=null;
				opts.forEach(function(o){if(o.value===swatch.dataset.value)opt=o;});
				if(opt&&opt.disabled){swatch.classList.add("kramar-swatch--disabled");swatch.setAttribute("aria-disabled","true");}
				else{swatch.classList.remove("kramar-swatch--disabled");swatch.removeAttribute("aria-disabled");}
			});
		});
	});

	// Ensure every group has exactly one tabbable swatch on load (none selected yet).
	document.querySelectorAll(".kramar-swatches").forEach(function(container){
		if(!container.querySelector(".kramar-swatch[tabindex=\"0\"]")){
			var first=container.querySelector(".kramar-swatch");
			if(first)first.setAttribute("tabindex","0");
		}
	});
})();
';

		wp_add_inline_script( 'wc-add-to-cart-variation', $js );
	}

	// -------------------------------------------------------------------------
	// Admin Scripts
	// -------------------------------------------------------------------------

	/**
	 * Enqueue WP media scripts and swatch admin JS on attribute term pages.
	 *
	 * Must run during admin_enqueue_scripts, not admin_footer.
	 *
	 * @since 1.0.0
	 */
	public function enqueue_term_media(): void {
		$screen = get_current_screen();
		if ( ! $screen ) {
			return;
		}
		if ( 'edit-tags' !== $screen->base && 'term' !== $screen->base ) {
			return;
		}
		$type = $this->get_taxonomy_swatch_type( $screen->taxonomy ?? '' );

		if ( 'color' === $type ) {
			wp_enqueue_script(
				'kramar-swatch-color-admin',
				KRAMAR_URL . 'assets/js/swatch-color-admin.js',
				[],
				KRAMAR_VERSION,
				true
			);
		}

		if ( 'image' === $type ) {
			wp_enqueue_media();
			wp_enqueue_script(
				'kramar-swatch-image-admin',
				KRAMAR_URL . 'assets/js/swatch-image-admin.js',
				[ 'media' ],
				KRAMAR_VERSION,
				true
			);
			wp_localize_script(
				'kramar-swatch-image-admin',
				'kramarSwatchL10n',
				[
					'selectTitle' => __( 'Select Swatch Image', 'kramar' ),
					'useImage'    => __( 'Use image', 'kramar' ),
				]
			);
		}
	}

	/**
	 * Enqueue swatch admin CSS and variation panel JS on product edit screen.
	 *
	 * @since 1.0.0
	 */
	public function enqueue_product_admin_assets(): void {
		$screen = get_current_screen();
		if ( ! $screen || 'product' !== $screen->id ) {
			return;
		}

		// Inline CSS for variation swatch previews in the product editor.
		wp_register_style( 'kramar-product-admin-css', false, [], KRAMAR_VERSION ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.NotProperlyEnqueued -- intentional: handle with no src, used only for wp_add_inline_style
		wp_enqueue_style( 'kramar-product-admin-css' );
		wp_add_inline_style(
			'kramar-product-admin-css',
			'.kramar-var-swatch{display:inline-block;width:20px;height:20px;border-radius:50%;border:2px solid rgba(0,0,0,.15);vertical-align:middle;margin-left:6px;}' .
			'.kramar-var-swatch--image{border-radius:3px;overflow:hidden;}' .
			'.kramar-var-swatch--image img{width:100%;height:100%;object-fit:cover;display:block;}' .
			'.kramar-var-swatch--button{width:auto;height:auto;border-radius:3px;padding:1px 6px;font-size:11px;line-height:16px;background:#f0f0f1;border:1px solid #ddd;}' .
			'.kramar-var-type{font-size:11px;color:#888;font-weight:400;margin-left:4px;vertical-align:middle;}'
		);

		// JS to hide upload_image for color-only variations (reads data-loop markers in DOM).
		wp_enqueue_script(
			'kramar-swatch-variation-admin',
			KRAMAR_URL . 'assets/js/swatch-variation-admin.js',
			[],
			KRAMAR_VERSION,
			true
		);
	}

	// -------------------------------------------------------------------------
	// Admin: Variation Swatch Preview
	// -------------------------------------------------------------------------

	public function render_variation_swatch_header( \WP_Post $variation, int $loop ): void {
		$variation_obj = wc_get_product( $variation->ID );
		if ( ! $variation_obj instanceof \WC_Product_Variation ) {
			return;
		}

		$attributes = $variation_obj->get_attributes( 'edit' );

		foreach ( $attributes as $taxonomy => $slug ) {
			if ( '' === $slug || 0 !== strpos( $taxonomy, 'pa_' ) ) {
				continue;
			}

			$type = $this->get_attribute_type( $taxonomy );
			$term = get_term_by( 'slug', $slug, $taxonomy );
			if ( ! $term instanceof \WP_Term ) {
				continue;
			}

			$type_labels = [
				'color'  => __( 'Color', 'kramar' ),
				'image'  => __( 'Image', 'kramar' ),
				'button' => __( 'Button', 'kramar' ),
			];
			$type_label = $type_labels[ $type ] ?? '';

			if ( 'color' === $type ) {
				$color = (string) get_term_meta( $term->term_id, 'kramar_swatch_color', true );
				$color = '' !== $color ? $color : '#cccccc';
				printf(
					'<span class="kramar-var-swatch" style="background-color:%s;" title="%s"></span><span class="kramar-var-type">%s</span>',
					esc_attr( $color ),
					esc_attr( $term->name . ': ' . $color ),
					esc_html( $type_label )
				);
			} elseif ( 'image' === $type ) {
				$image_id  = (int) get_term_meta( $term->term_id, 'kramar_swatch_image', true );
				$image_url = $this->get_swatch_image_url( $image_id );
				if ( '' !== $image_url ) {
					printf(
						'<span class="kramar-var-swatch kramar-var-swatch--image" title="%s"><img src="%s" alt=""></span><span class="kramar-var-type">%s</span>',
						esc_attr( $term->name ),
						esc_url( $image_url ),
						esc_html( $type_label )
					);
				}
			} elseif ( 'button' === $type ) {
				printf(
					'<span class="kramar-var-swatch kramar-var-swatch--button">%s</span><span class="kramar-var-type">%s</span>',
					esc_html( $term->name ),
					esc_html( $type_label )
				);
			}
		}
	}

	/**
	 * Show swatch preview inside the expanded variation panel and hide the
	 * default image upload for color/button types (swatch IS the visual).
	 *
	 * @since 1.0.0
	 *
	 * @param int      $loop           Position in the loop.
	 * @param array    $variation_data Variation data.
	 * @param \WP_Post $variation      Variation post object.
	 */
	public function render_variation_swatch_panel( int $loop, array $variation_data, \WP_Post $variation ): void {
		$variation_obj = wc_get_product( $variation->ID );
		if ( ! $variation_obj instanceof \WC_Product_Variation ) {
			return;
		}

		$attributes  = $variation_obj->get_attributes( 'edit' );
		$has_color   = false;
		$has_image   = false;

		foreach ( $attributes as $taxonomy => $slug ) {
			if ( '' === $slug || 0 !== strpos( $taxonomy, 'pa_' ) ) {
				continue;
			}

			$type = $this->get_attribute_type( $taxonomy );
			$term = get_term_by( 'slug', $slug, $taxonomy );
			if ( ! $term instanceof \WP_Term ) {
				continue;
			}

			if ( 'color' === $type ) {
				$has_color = true;
				$color     = (string) get_term_meta( $term->term_id, 'kramar_swatch_color', true );
				$color     = '' !== $color ? $color : '#cccccc';
				$edit_url  = get_edit_term_link( $term->term_id, $taxonomy );
				?>
				<div class="kramar-variation-swatch-preview" style="margin-bottom:12px;">
					<a href="<?php echo esc_url( (string) $edit_url ); ?>" style="display:inline-flex;align-items:center;gap:8px;text-decoration:none;color:inherit;" title="<?php esc_attr_e( 'Edit term swatch', 'kramar' ); ?>">
						<span style="display:inline-block;width:48px;height:48px;border-radius:50%;background:<?php echo esc_attr( $color ); ?>;border:2px solid rgba(0,0,0,.1);"></span>
						<span>
							<strong><?php echo esc_html( $term->name ); ?></strong><br>
							<code style="font-size:12px;"><?php echo esc_html( $color ); ?></code>
						</span>
					</a>
				</div>
				<?php
			} elseif ( 'image' === $type ) {
				$has_image  = true;
				$image_id   = (int) get_term_meta( $term->term_id, 'kramar_swatch_image', true );
				$image_url  = $this->get_swatch_image_url( $image_id );
				$edit_url   = get_edit_term_link( $term->term_id, $taxonomy );
				if ( '' !== $image_url ) {
					?>
					<div class="kramar-variation-swatch-preview" style="margin-bottom:12px;">
						<a href="<?php echo esc_url( (string) $edit_url ); ?>" style="display:inline-flex;align-items:center;gap:8px;text-decoration:none;color:inherit;" title="<?php esc_attr_e( 'Edit term swatch', 'kramar' ); ?>">
							<img src="<?php echo esc_url( $image_url ); ?>" alt="" style="width:48px;height:48px;object-fit:cover;border-radius:4px;border:1px solid rgba(0,0,0,.1);" />
							<strong><?php echo esc_html( $term->name ); ?></strong>
						</a>
					</div>
					<?php
				}
			}
		}

		if ( $has_color && ! $has_image ) {
			printf(
				'<span class="kramar-hide-upload" data-loop="%d" style="display:none;"></span>',
				(int) $loop
			);
		}
	}

	// -------------------------------------------------------------------------
	// Thumbnail Management
	// -------------------------------------------------------------------------

	/**
	 * Regenerate swatch thumbnails when the size setting changes.
	 *
	 * Deletes old kramar_swatch size files and generates new ones for every
	 * attachment used as a swatch image.
	 *
	 * @since 1.0.0
	 *
	 * @param int $new_size New swatch size in CSS px.
	 * @param int $old_size Previous swatch size in CSS px.
	 */
	private function regenerate_swatch_thumbnails( int $new_size, int $old_size ): void {
		$attachment_ids = $this->get_all_swatch_attachment_ids();
		if ( empty( $attachment_ids ) ) {
			return;
		}

		$new_px = max( 16, $new_size * 2 );

		foreach ( $attachment_ids as $attachment_id ) {
			$this->delete_swatch_size_file( $attachment_id );
			$this->generate_swatch_size( $attachment_id, $new_px );
		}
	}

	/**
	 * Collect all attachment IDs used as swatch images across all taxonomies.
	 *
	 * @since 1.0.0
	 *
	 * @return int[]
	 */
	private function get_all_swatch_attachment_ids(): array {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery
		$rows = $wpdb->get_col(
			"SELECT DISTINCT meta_value FROM {$wpdb->termmeta} WHERE meta_key = 'kramar_swatch_image' AND meta_value > 0"
		);

		return array_map( 'absint', $rows );
	}

	/**
	 * Delete the kramar_swatch size file and metadata for an attachment.
	 *
	 * @since 1.0.0
	 *
	 * @param int $attachment_id Attachment ID.
	 */
	private function delete_swatch_size_file( int $attachment_id ): void {
		$meta = wp_get_attachment_metadata( $attachment_id );
		if ( ! is_array( $meta ) || empty( $meta['sizes'][ self::IMAGE_SIZE ] ) ) {
			return;
		}

		$upload_dir = wp_get_upload_dir();
		$base_dir   = trailingslashit( $upload_dir['basedir'] );
		$file_dir   = trailingslashit( dirname( $meta['file'] ) );
		$file_path  = $base_dir . $file_dir . $meta['sizes'][ self::IMAGE_SIZE ]['file'];

		if ( file_exists( $file_path ) ) {
			wp_delete_file( $file_path );
		}

		unset( $meta['sizes'][ self::IMAGE_SIZE ] );
		wp_update_attachment_metadata( $attachment_id, $meta );
	}

	/**
	 * Generate the kramar_swatch size for an attachment.
	 *
	 * @since 1.0.0
	 *
	 * @param int $attachment_id Attachment ID.
	 * @param int $px            Target dimension in pixels.
	 */
	private function generate_swatch_size( int $attachment_id, int $px ): void {
		$file = get_attached_file( $attachment_id );
		if ( ! $file || ! file_exists( $file ) ) {
			return;
		}

		$editor = wp_get_image_editor( $file );
		if ( is_wp_error( $editor ) ) {
			return;
		}

		$editor->resize( $px, $px, true );

		$upload_dir = wp_get_upload_dir();
		$info       = pathinfo( $file );
		$dir        = trailingslashit( $info['dirname'] );
		$ext        = '.' . $info['extension'];
		$new_file   = $dir . $info['filename'] . '-' . $px . 'x' . $px . $ext;

		$saved = $editor->save( $new_file );
		if ( is_wp_error( $saved ) ) {
			return;
		}

		$meta = wp_get_attachment_metadata( $attachment_id );
		if ( ! is_array( $meta ) ) {
			return;
		}

		$meta['sizes'][ self::IMAGE_SIZE ] = [
			'file'      => basename( $saved['path'] ),
			'width'     => $saved['width'],
			'height'    => $saved['height'],
			'mime-type' => $saved['mime-type'],
		];

		wp_update_attachment_metadata( $attachment_id, $meta );
	}

	/**
	 * Generate the swatch thumbnail for an attachment if it doesn't exist yet.
	 *
	 * @since 1.0.0
	 *
	 * @param int $attachment_id Attachment ID.
	 */
	private function ensure_swatch_size_exists( int $attachment_id ): void {
		$meta = wp_get_attachment_metadata( $attachment_id );
		if ( is_array( $meta ) && ! empty( $meta['sizes'][ self::IMAGE_SIZE ] ) ) {
			return;
		}

		$size = (int) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'swatch_size' );
		$px   = max( 16, $size * 2 );
		$this->generate_swatch_size( $attachment_id, $px );
	}

	/**
	 * Get the swatch image URL, generating the size on-the-fly if needed.
	 *
	 * @since 1.0.0
	 *
	 * @param int $attachment_id Attachment ID.
	 * @return string Image URL or empty string.
	 */
	private function get_swatch_image_url( int $attachment_id ): string {
		if ( $attachment_id <= 0 ) {
			return '';
		}

		$this->ensure_swatch_size_exists( $attachment_id );

		$url = wp_get_attachment_image_url( $attachment_id, self::IMAGE_SIZE );
		return $url ? (string) $url : '';
	}

	// -------------------------------------------------------------------------
	// Helpers
	// -------------------------------------------------------------------------

	/**
	 * Look up the WooCommerce attribute type for a given taxonomy name.
	 *
	 * @since 1.0.0
	 *
	 * @param string $attribute_name Taxonomy name (e.g. `pa_color`).
	 * @return string Attribute type, or `select` as fallback.
	 */
	private function get_attribute_type( string $attribute_name ): string {
		$taxonomy_name        = str_replace( 'pa_', '', $attribute_name );
		$attribute_taxonomies = wc_get_attribute_taxonomies();
		foreach ( $attribute_taxonomies as $attr ) {
			if ( $attr->attribute_name === $taxonomy_name ) {
				return (string) $attr->attribute_type;
			}
		}
		return 'select';
	}

	/**
	 * Look up the swatch type for a given taxonomy slug.
	 *
	 * @since 1.0.0
	 *
	 * @param string $taxonomy Full taxonomy name (e.g. `pa_colour`).
	 * @return string Attribute type or empty string if not a swatch type.
	 */
	private function get_taxonomy_swatch_type( string $taxonomy ): string {
		if ( 0 !== strpos( $taxonomy, 'pa_' ) ) {
			return '';
		}
		return $this->get_attribute_type( $taxonomy );
	}

	/**
	 * {@inheritDoc}
	 */
	public function render_extra_settings( string $option_name ): void {
		$size    = (int) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'swatch_size' );
		$tooltip = (bool) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'show_tooltip' );
		?>
		<div class="kramar-module__extra">
			<p>
				<label for="kramar-swatch-size"><?php esc_html_e( 'Swatch size', 'kramar' ); ?></label>
				<input
					type="number"
					id="kramar-swatch-size"
					name="<?php echo esc_attr( $option_name ); ?>[products][swatches][swatch_size]"
					value="<?php echo esc_attr( (string) $size ); ?>"
					min="16"
					max="96"
					step="1"
					class="small-text"
				/> <?php esc_html_e( 'px', 'kramar' ); ?>
			</p>
			<p>
				<label>
					<input
						type="checkbox"
						name="<?php echo esc_attr( $option_name ); ?>[products][swatches][show_tooltip]"
						value="1"
						<?php checked( $tooltip ); ?>
					/>
					<?php esc_html_e( 'Show term name on hover (browser tooltip)', 'kramar' ); ?>
				</label>
			</p>
			<p class="description">
				<?php
				printf(
					/* translators: %s: admin URL */
					wp_kses_post( __( 'Swatch types are set per <strong>global</strong> attribute (Products &rarr; Attributes) — custom per-product attributes cannot use swatches. Go to <a href="%s">Products &rarr; Attributes</a> and choose Color, Image, or Button as the attribute type, then configure each term with its swatch value. Terms left without a swatch value fall back to a plain grey circle.', 'kramar' ) ),
					esc_url( admin_url( 'edit.php?post_type=product&page=product_attributes' ) )
				);
				?>
			</p>
		</div>
		<?php
	}
}
