<?php
/**
 * EU Product Compliance module.
 *
 * @package Kramar
 * @since   1.0.0
 */

declare( strict_types=1 );

namespace Kramar\Modules\Compliance;

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

/**
 * EU Product Compliance module.
 *
 * Bundles three independent EU/DE compliance features:
 *   1. Unit price (Grundpreis) — per weight/volume/length/area/count.
 *   2. Delivery time taxonomy — displayed on product + shop loop.
 *   3. Tax & shipping notice — appended to price HTML in shop/product context.
 *
 * @since 1.0.0
 */
final class EU_Product_Compliance extends \Kramar\Module {

	// -------------------------------------------------------------------------
	// Conversion factor table
	// Unit price = (displayed_price / content_in_base_unit) * base_quantity
	// -------------------------------------------------------------------------

	/**
	 * Measure-group conversion factors.
	 *
	 * Inner key: unit slug → factor relative to the group's base unit (1 base unit = 1.0).
	 * Example: 'g' → 0.001 means 1 g = 0.001 kg.
	 *
	 * Groups: weight (base: kg), volume (base: l), length (base: m), area (base: m2), count (base: piece).
	 *
	 * @since 1.0.0
	 * @var array<string, array<string, float>>
	 */
	private const CONVERSION = [
		'weight' => [
			'mg' => 0.000001,
			'g'  => 0.001,
			'kg' => 1.0,
		],
		'volume' => [
			'ml' => 0.001,
			'cl' => 0.01,
			'l'  => 1.0,
		],
		'length' => [
			'mm' => 0.001,
			'cm' => 0.01,
			'm'  => 1.0,
		],
		'area' => [
			'cm2' => 0.0001,
			'm2'  => 1.0,
		],
		'count' => [
			'piece' => 1.0,
		],
	];

	/**
	 * Taxonomy slug for delivery times.
	 *
	 * @since 1.0.0
	 */
	private const DELIVERY_TAX = 'kramar_delivery_time';

	// -------------------------------------------------------------------------
	// Module identity
	// -------------------------------------------------------------------------

	/**
	 * {@inheritDoc}
	 */
	public function get_slug(): string {
		return 'eu-product-compliance';
	}

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

	/**
	 * {@inheritDoc}
	 */
	public function get_name(): string {
		return __( 'EU Product Compliance', 'kramar' );
	}

	/**
	 * {@inheritDoc}
	 */
	public function get_description(): string {
		return __( 'Three independent EU/NL compliance features: unit price (Grundpreis), per-product delivery-time estimates (set a shop-wide default, override per product in the Shipping tab), and a tax/shipping notice on prices.', 'kramar' );
	}

	// -------------------------------------------------------------------------
	// Settings
	// -------------------------------------------------------------------------

	/**
	 * {@inheritDoc}
	 */
	public function get_default_settings(): array {
		return [
			'enabled'                => false,
			// Unit price feature.
			'unit_price_enabled'     => true,
			// Delivery time feature.
			'delivery_time_enabled'  => true,
			// Term ID of the global default delivery time (0 = none set yet).
			'delivery_time_default'  => 0,
			// Tax/shipping notice feature.
			'tax_notice_enabled'     => true,
			/* translators: placeholders are {tax} and {shipping} — do not translate the placeholder names. */
			'tax_notice_text'        => __( '{tax} | {shipping}', 'kramar' ),
			'tax_notice_tax_text'    => __( 'incl. VAT', 'kramar' ),
			'tax_notice_excl_text'   => __( 'excl. VAT', 'kramar' ),
			'tax_notice_ship_text'   => __( 'plus shipping', 'kramar' ),
			'tax_notice_ship_url'    => '',
		];
	}

	/**
	 * {@inheritDoc}
	 */
	public function sanitize_settings( array $input ): array {
		return [
			'enabled'                => ! empty( $input['enabled'] ),
			'unit_price_enabled'     => ! empty( $input['unit_price_enabled'] ),
			'delivery_time_enabled'  => ! empty( $input['delivery_time_enabled'] ),
			// Term ID of the global default delivery time (0 = none).
			'delivery_time_default'  => absint( $input['delivery_time_default'] ?? 0 ),
			'tax_notice_enabled'     => ! empty( $input['tax_notice_enabled'] ),
			'tax_notice_text'        => sanitize_text_field( $input['tax_notice_text'] ?? '{tax} | {shipping}' ),
			'tax_notice_tax_text'    => sanitize_text_field( $input['tax_notice_tax_text'] ?? '' ),
			'tax_notice_excl_text'   => sanitize_text_field( $input['tax_notice_excl_text'] ?? '' ),
			'tax_notice_ship_text'   => sanitize_text_field( $input['tax_notice_ship_text'] ?? '' ),
			'tax_notice_ship_url'    => esc_url_raw( $input['tax_notice_ship_url'] ?? '' ),
		];
	}

	// -------------------------------------------------------------------------
	// Boot
	// -------------------------------------------------------------------------

	/**
	 * {@inheritDoc}
	 */
	protected function run(): void {
		// Register delivery-time taxonomy unconditionally so term management is
		// available even when the display feature is toggled off.
		add_action( 'init', [ $this, 'register_delivery_taxonomy' ], 5 );

		// -- Unit price --
		if ( $this->s( 'unit_price_enabled' ) ) {
			// Product editor: main product fields.
			add_action( 'woocommerce_product_options_general_product_data', [ $this, 'render_unit_price_fields' ] );
			// Save main product meta.
			add_action( 'woocommerce_process_product_meta', [ $this, 'save_unit_price_meta' ] );

			// Variable product: per-variation fields.
			add_action( 'woocommerce_product_after_variable_attributes', [ $this, 'render_variation_unit_price_fields' ], 10, 3 );
			// Save variation meta.
			add_action( 'woocommerce_save_product_variation', [ $this, 'save_variation_unit_price_meta' ], 10, 2 );

			// Frontend display.
			add_action( 'woocommerce_single_product_summary', [ $this, 'display_unit_price' ], 11 );
			add_action( 'woocommerce_after_shop_loop_item_title', [ $this, 'display_unit_price' ], 11 );

			// Variable products: pass unit price data per variation via filter.
			add_filter( 'woocommerce_available_variation', [ $this, 'inject_variation_unit_price' ], 10, 3 );

			// Inline CSS + JS.
			add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_frontend_assets' ] );
		}

		// -- Delivery time --
		if ( $this->s( 'delivery_time_enabled' ) ) {
			// Per-product control: a single <select> in the Product data →
			// Shipping tab (not the sidebar taxonomy box — see register above).
			add_action( 'woocommerce_product_options_shipping', [ $this, 'render_delivery_time_field' ] );
			add_action( 'woocommerce_process_product_meta', [ $this, 'save_delivery_time_field' ] );

			// Keep placement consistent: render the delivery time just below the
			// price on both the single product summary AND the shop/related loop.
			// WC prints the loop price at priority 10, so we sit after it (12) —
			// previously priority 8 pushed it *above* the price on the loop only.
			add_action( 'woocommerce_single_product_summary', [ $this, 'display_delivery_time' ], 27 );
			add_action( 'woocommerce_after_shop_loop_item_title', [ $this, 'display_delivery_time' ], 12 );
		}

		// -- Tax / shipping notice --
		if ( $this->s( 'tax_notice_enabled' ) ) {
			add_filter( 'woocommerce_get_price_html', [ $this, 'append_price_notice' ], 20, 2 );
		}
	}

	// -------------------------------------------------------------------------
	// Taxonomy: kramar_delivery_time
	// -------------------------------------------------------------------------

	/**
	 * Register the `kramar_delivery_time` taxonomy.
	 *
	 * Hierarchical so shop owners can nest "Standard" → "2-3 days". The admin
	 * column + quick-edit integration is handled by WooCommerce automatically for
	 * product taxonomies with `show_in_menu` true.
	 *
	 * @since 1.0.0
	 */
	public function register_delivery_taxonomy(): void {
		$labels = [
			'name'              => _x( 'Delivery Times', 'taxonomy general name', 'kramar' ),
			'singular_name'     => _x( 'Delivery Time', 'taxonomy singular name', 'kramar' ),
			'search_items'      => __( 'Search Delivery Times', 'kramar' ),
			'all_items'         => __( 'All Delivery Times', 'kramar' ),
			'parent_item'       => __( 'Parent Delivery Time', 'kramar' ),
			'parent_item_colon' => __( 'Parent Delivery Time:', 'kramar' ),
			'edit_item'         => __( 'Edit Delivery Time', 'kramar' ),
			'update_item'       => __( 'Update Delivery Time', 'kramar' ),
			'add_new_item'      => __( 'Add New Delivery Time', 'kramar' ),
			'new_item_name'     => __( 'New Delivery Time Name', 'kramar' ),
			'menu_name'         => __( 'Delivery Times', 'kramar' ),
		];

		register_taxonomy(
			self::DELIVERY_TAX,
			'product',
			[
				'labels'            => $labels,
				'hierarchical'      => true,
				'public'            => true,
				'show_ui'           => true,
				'show_in_menu'      => true,
				'show_admin_column' => true,
				'show_in_rest'      => true,
				// Suppress the default checkbox metabox in the product editor's
				// right sidebar: a product has exactly one delivery time, so the
				// per-product control is a single <select> in the Product data →
				// Shipping tab instead (see render_delivery_time_field()). Term
				// management stays under Products → Delivery Times via show_in_menu.
				'meta_box_cb'       => false,
				'rewrite'           => [ 'slug' => 'delivery-time' ],
			]
		);
	}

	/**
	 * All delivery-time terms as [ id => name ], for building <select> controls.
	 *
	 * @since 1.0.0
	 *
	 * @return array<int, string>
	 */
	private function get_delivery_time_options(): array {
		$terms = get_terms(
			[
				'taxonomy'   => self::DELIVERY_TAX,
				'hide_empty' => false,
			]
		);

		$options = [];
		if ( is_array( $terms ) ) {
			foreach ( $terms as $term ) {
				if ( $term instanceof \WP_Term ) {
					$options[ $term->term_id ] = $term->name;
				}
			}
		}
		return $options;
	}

	// -------------------------------------------------------------------------
	// Delivery time: per-product admin field (Shipping tab)
	// -------------------------------------------------------------------------

	/**
	 * Render the single delivery-time <select> in the Product data → Shipping tab.
	 *
	 * A product has exactly one delivery time, so this is a single select (never
	 * the multi-checkbox taxonomy box). The empty option means "inherit the
	 * global default" (see resolve_delivery_time()).
	 *
	 * @since 1.0.0
	 */
	public function render_delivery_time_field(): void {
		global $post;

		if ( ! $post instanceof \WP_Post ) {
			return;
		}

		$terms    = $this->get_delivery_time_options();
		$assigned = get_the_terms( $post->ID, self::DELIVERY_TAX );
		$current  = ( is_array( $assigned ) && ! empty( $assigned ) && $assigned[0] instanceof \WP_Term )
			? (string) $assigned[0]->term_id
			: '';

		$options = [ '' => __( '— Default (use global delivery time) —', 'kramar' ) ];
		foreach ( $terms as $id => $name ) {
			$options[ (string) $id ] = $name;
		}

		echo '<div class="options_group kramar-delivery-time-group">';
		woocommerce_wp_select(
			[
				'id'          => '_kramar_delivery_time',
				'label'       => __( 'Delivery time', 'kramar' ),
				'value'       => $current,
				'options'     => $options,
				'description' => empty( $terms )
					? __( 'No delivery times defined yet — add them under Products → Delivery Times.', 'kramar' )
					: __( 'Shown on the product and shop pages. Leave on “Default” to use the global delivery time.', 'kramar' ),
				'desc_tip'    => true,
			]
		);
		echo '</div>';
	}

	/**
	 * Persist the product's single delivery-time term (replace mode → one or none).
	 *
	 * @since 1.0.0
	 *
	 * @param int $product_id Post ID of the product being saved.
	 */
	public function save_delivery_time_field( int $product_id ): void {
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- WC verifies nonce before firing woocommerce_process_product_meta.
		$term_id = isset( $_POST['_kramar_delivery_time'] ) ? absint( wp_unslash( $_POST['_kramar_delivery_time'] ) ) : 0;
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		// Replace mode (append = false) guarantees at most one assigned term,
		// even if a legacy multi-term assignment existed from the old checkbox UI.
		wp_set_object_terms(
			$product_id,
			$term_id > 0 ? [ $term_id ] : [],
			self::DELIVERY_TAX,
			false
		);
	}

	// -------------------------------------------------------------------------
	// Unit price: admin fields
	// -------------------------------------------------------------------------

	/**
	 * Render unit price fields in the General tab of the product editor.
	 *
	 * All four meta keys share the `kramar-unit-price-` CSS prefix for JS targeting.
	 *
	 * @since 1.0.0
	 */
	public function render_unit_price_fields(): void {
		global $post;

		$product_id = (int) $post->ID;

		$content   = (string) get_post_meta( $product_id, '_kramar_unit_content', true );
		$unit      = (string) get_post_meta( $product_id, '_kramar_unit', true );
		$base      = (string) get_post_meta( $product_id, '_kramar_base', true );
		$base_unit = (string) get_post_meta( $product_id, '_kramar_base_unit', true );

		echo '<div class="options_group kramar-unit-price-group">';

		echo '<p class="form-field"><strong>' . esc_html__( 'Unit Price (Grundpreis)', 'kramar' ) . '</strong></p>';

		woocommerce_wp_text_input(
			[
				'id'                => '_kramar_unit_content',
				'label'             => __( 'Content', 'kramar' ),
				'value'             => $content,
				'type'              => 'number',
				'custom_attributes' => [ 'step' => 'any', 'min' => '0' ],
				/* translators: number field for product content amount, e.g. 500 */
				'description'       => __( 'Numeric amount of the product (e.g. 500)', 'kramar' ),
				'desc_tip'          => true,
			]
		);

		woocommerce_wp_text_input(
			[
				'id'          => '_kramar_unit',
				'label'       => __( 'Unit', 'kramar' ),
				'value'       => $unit,
				/* translators: unit label for content amount, e.g. g, ml, cm */
				'description' => __( 'Unit of measurement for the content (e.g. g, ml, cm)', 'kramar' ),
				'desc_tip'    => true,
			]
		);

		woocommerce_wp_text_input(
			[
				'id'                => '_kramar_base',
				'label'             => __( 'Base quantity', 'kramar' ),
				'value'             => $base,
				'type'              => 'number',
				'custom_attributes' => [ 'step' => 'any', 'min' => '0' ],
				/* translators: number field for reference/base quantity, e.g. 1 */
				'description'       => __( 'Reference quantity for the unit price (e.g. 1)', 'kramar' ),
				'desc_tip'          => true,
			]
		);

		woocommerce_wp_text_input(
			[
				'id'          => '_kramar_base_unit',
				'label'       => __( 'Base unit', 'kramar' ),
				'value'       => $base_unit,
				/* translators: base unit label, e.g. kg, l, m */
				'description' => __( 'Unit for the base quantity (e.g. kg, l, m). Must be in the same measure group as the content unit.', 'kramar' ),
				'desc_tip'    => true,
			]
		);

		echo '</div>';
	}

	/**
	 * Save unit price meta for simple/external/grouped products.
	 *
	 * Nonce + capability are verified by WooCommerce before this action fires.
	 *
	 * @since 1.0.0
	 *
	 * @param int $product_id Post ID of the product being saved.
	 */
	public function save_unit_price_meta( int $product_id ): void {
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- WC verifies nonce before firing woocommerce_process_product_meta.
		$content   = isset( $_POST['_kramar_unit_content'] ) ? sanitize_text_field( wp_unslash( $_POST['_kramar_unit_content'] ) ) : '';
		$unit      = isset( $_POST['_kramar_unit'] ) ? sanitize_text_field( wp_unslash( $_POST['_kramar_unit'] ) ) : '';
		$base      = isset( $_POST['_kramar_base'] ) ? sanitize_text_field( wp_unslash( $_POST['_kramar_base'] ) ) : '';
		$base_unit = isset( $_POST['_kramar_base_unit'] ) ? sanitize_text_field( wp_unslash( $_POST['_kramar_base_unit'] ) ) : '';
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		update_post_meta( $product_id, '_kramar_unit_content', $content );
		update_post_meta( $product_id, '_kramar_unit', $unit );
		update_post_meta( $product_id, '_kramar_base', $base );
		update_post_meta( $product_id, '_kramar_base_unit', $base_unit );
	}

	// -------------------------------------------------------------------------
	// Unit price: variation admin fields
	// -------------------------------------------------------------------------

	/**
	 * Render unit price fields inside the variation accordion.
	 *
	 * @since 1.0.0
	 *
	 * @param int     $loop           Variation loop index.
	 * @param array<string, mixed> $variation_data Variation data (unused here).
	 * @param \WP_Post $variation     Variation post object.
	 */
	public function render_variation_unit_price_fields( int $loop, array $variation_data, \WP_Post $variation ): void {
		$vid = $variation->ID;

		$content   = (string) get_post_meta( $vid, '_kramar_unit_content', true );
		$unit      = (string) get_post_meta( $vid, '_kramar_unit', true );
		$base      = (string) get_post_meta( $vid, '_kramar_base', true );
		$base_unit = (string) get_post_meta( $vid, '_kramar_base_unit', true );

		echo '<div class="kramar-unit-price-variation-fields" style="width:100%;padding:5px 9px;">';
		echo '<p><strong>' . esc_html__( 'Unit Price (Grundpreis) — variation override', 'kramar' ) . '</strong></p>';

		// Content.
		echo '<p class="form-row form-row-first">';
		echo '<label>' . esc_html__( 'Content', 'kramar' ) . '</label>';
		printf(
			'<input type="number" step="any" min="0" name="_kramar_unit_content[%d]" value="%s" class="short" placeholder="%s" />',
			(int) $loop,
			esc_attr( $content ),
			esc_attr__( 'e.g. 500', 'kramar' )
		);
		echo '</p>';

		// Unit.
		echo '<p class="form-row form-row-last">';
		echo '<label>' . esc_html__( 'Unit', 'kramar' ) . '</label>';
		printf(
			'<input type="text" name="_kramar_unit[%d]" value="%s" class="short" placeholder="%s" />',
			(int) $loop,
			esc_attr( $unit ),
			esc_attr__( 'e.g. g, ml', 'kramar' )
		);
		echo '</p>';

		// Base quantity.
		echo '<p class="form-row form-row-first">';
		echo '<label>' . esc_html__( 'Base quantity', 'kramar' ) . '</label>';
		printf(
			'<input type="number" step="any" min="0" name="_kramar_base[%d]" value="%s" class="short" placeholder="%s" />',
			(int) $loop,
			esc_attr( $base ),
			esc_attr__( 'e.g. 1', 'kramar' )
		);
		echo '</p>';

		// Base unit.
		echo '<p class="form-row form-row-last">';
		echo '<label>' . esc_html__( 'Base unit', 'kramar' ) . '</label>';
		printf(
			'<input type="text" name="_kramar_base_unit[%d]" value="%s" class="short" placeholder="%s" />',
			(int) $loop,
			esc_attr( $base_unit ),
			esc_attr__( 'e.g. kg, l', 'kramar' )
		);
		echo '</p>';

		echo '</div>';
	}

	/**
	 * Save unit price meta for a variation.
	 *
	 * Nonce + capability are verified by WooCommerce before this action fires.
	 *
	 * @since 1.0.0
	 *
	 * @param int $variation_id Variation post ID.
	 * @param int $loop         Variation loop index.
	 */
	public function save_variation_unit_price_meta( int $variation_id, int $loop ): void {
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- WC verifies nonce before firing woocommerce_save_product_variation.
		$content   = isset( $_POST['_kramar_unit_content'][ $loop ] ) ? sanitize_text_field( wp_unslash( $_POST['_kramar_unit_content'][ $loop ] ) ) : '';
		$unit      = isset( $_POST['_kramar_unit'][ $loop ] ) ? sanitize_text_field( wp_unslash( $_POST['_kramar_unit'][ $loop ] ) ) : '';
		$base      = isset( $_POST['_kramar_base'][ $loop ] ) ? sanitize_text_field( wp_unslash( $_POST['_kramar_base'][ $loop ] ) ) : '';
		$base_unit = isset( $_POST['_kramar_base_unit'][ $loop ] ) ? sanitize_text_field( wp_unslash( $_POST['_kramar_base_unit'][ $loop ] ) ) : '';
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		update_post_meta( $variation_id, '_kramar_unit_content', $content );
		update_post_meta( $variation_id, '_kramar_unit', $unit );
		update_post_meta( $variation_id, '_kramar_base', $base );
		update_post_meta( $variation_id, '_kramar_base_unit', $base_unit );
	}

	// -------------------------------------------------------------------------
	// Unit price: frontend display
	// -------------------------------------------------------------------------

	/**
	 * Output the unit price span on product page and shop loop.
	 *
	 * For simple/external products the price is resolved immediately.
	 * For variable products, the span is left empty and populated by JS
	 * (`woocommerce_available_variation` passes the pre-computed string).
	 *
	 * @since 1.0.0
	 */
	public function display_unit_price(): void {
		global $product;

		if ( ! $product instanceof \WC_Product ) {
			return;
		}

		if ( $product->is_type( 'variable' ) ) {
			// Empty span — JS fills it when the customer selects a variation.
			echo '<span class="kramar-unit-price" aria-live="polite"></span>';
			return;
		}

		$unit_price = $this->compute_unit_price_for_product( $product );
		if ( '' === $unit_price ) {
			return;
		}

		printf(
			'<span class="kramar-unit-price">%s</span>',
			wp_kses_post( $unit_price )
		);
	}

	/**
	 * Inject computed unit price string into WooCommerce variation data (AJAX response).
	 *
	 * The JS handler reads `variation.kramar_unit_price` and updates the span.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, mixed> $data       Variation data array passed to JS.
	 * @param \WC_Product_Variable $product    Parent variable product (unused here).
	 * @param \WC_Product_Variation $variation Variation product object.
	 * @return array<string, mixed> Modified variation data.
	 */
	public function inject_variation_unit_price( array $data, \WC_Product_Variable $product, \WC_Product_Variation $variation ): array {
		$data['kramar_unit_price'] = $this->compute_unit_price_for_product( $variation );
		return $data;
	}

	/**
	 * Compute the formatted unit price string for a product/variation.
	 *
	 * Formula: unit_price = (displayed_price / content_in_base_unit) * base_quantity
	 *
	 * Returns empty string when any required value is missing, zero, or when units
	 * are in different measure groups (no conversion path).
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Product $product Product or variation to compute for.
	 * @return string Formatted HTML price string, or empty string on failure.
	 */
	public function compute_unit_price_for_product( \WC_Product $product ): string {
		$product_id = $product->get_id();

		$content_str   = (string) get_post_meta( $product_id, '_kramar_unit_content', true );
		$unit          = strtolower( trim( (string) get_post_meta( $product_id, '_kramar_unit', true ) ) );
		$base_str      = (string) get_post_meta( $product_id, '_kramar_base', true );
		$base_unit     = strtolower( trim( (string) get_post_meta( $product_id, '_kramar_base_unit', true ) ) );

		$content = (float) $content_str;
		$base    = (float) $base_str;

		if ( $content <= 0.0 || $base <= 0.0 || '' === $unit || '' === $base_unit ) {
			return '';
		}

		// Resolve conversion factors.
		$content_factor = $this->get_conversion_factor( $unit );
		$base_factor    = $this->get_conversion_factor( $base_unit );

		if ( null === $content_factor || null === $base_factor ) {
			return '';
		}

		// Both units must belong to the same measure group.
		if ( $this->get_measure_group( $unit ) !== $this->get_measure_group( $base_unit ) ) {
			return '';
		}

		// content expressed in base group unit.
		$content_in_base = $content * $content_factor;
		// base_quantity expressed in base group unit.
		$base_in_base    = $base * $base_factor;

		if ( $content_in_base <= 0.0 ) {
			return '';
		}

		// Use wc_get_price_to_display() so the price respects tax display settings
		// and follows the currently displayed (potentially sale) price.
		$display_price = (float) wc_get_price_to_display( $product );

		if ( $display_price <= 0.0 ) {
			return '';
		}

		$unit_price = ( $display_price / $content_in_base ) * $base_in_base;

		// Format using WooCommerce's price formatter.
		$formatted = wc_price( $unit_price );

		// Format base quantity: show as integer when whole, otherwise strip trailing zeros.
		$base_formatted = ( $base === floor( $base ) )
			? (string) (int) $base
			: rtrim( rtrim( number_format( $base, 6, '.', '' ), '0' ), '.' );

		/* translators: 1: formatted price, 2: base quantity number, 3: base unit label (e.g. kg, l, m) */
		$template = __( '%1$s / %2$s %3$s', 'kramar' );

		return sprintf(
			$template,
			$formatted,
			esc_html( $base_formatted ),
			esc_html( $base_unit )
		);
	}

	/**
	 * Get the conversion factor (to group base unit) for a unit slug.
	 *
	 * @since 1.0.0
	 *
	 * @param string $unit Unit slug.
	 * @return float|null Conversion factor, or null if unit is not recognised.
	 */
	private function get_conversion_factor( string $unit ): ?float {
		foreach ( self::CONVERSION as $factors ) {
			if ( isset( $factors[ $unit ] ) ) {
				return $factors[ $unit ];
			}
		}
		return null;
	}

	/**
	 * Get the measure group name for a unit slug.
	 *
	 * @since 1.0.0
	 *
	 * @param string $unit Unit slug.
	 * @return string|null Group name ('weight', 'volume', …), or null if not found.
	 */
	private function get_measure_group( string $unit ): ?string {
		foreach ( self::CONVERSION as $group => $factors ) {
			if ( isset( $factors[ $unit ] ) ) {
				return $group;
			}
		}
		return null;
	}

	// -------------------------------------------------------------------------
	// Unit price: frontend assets
	// -------------------------------------------------------------------------

	/**
	 * Enqueue structural CSS and variation-switch JS on the frontend.
	 *
	 * CSS is delivered via wp_add_inline_style() on a registered front handle
	 * to stay .org-compliant (no inline <style> echo).
	 * JS is plain vanilla, no jQuery.
	 *
	 * @since 1.0.0
	 */
	public function enqueue_frontend_assets(): void {
		if ( ! is_singular( 'product' ) && ! is_shop() && ! is_product_category() && ! is_product_tag() ) {
			return;
		}

		// Ensure a handle exists for inline style attachment.
		// We piggyback on 'woocommerce-general' (always enqueued on WC pages).
		$handle = 'woocommerce-general';
		if ( ! wp_style_is( $handle, 'registered' ) && ! wp_style_is( $handle, 'enqueued' ) ) {
			wp_register_style( 'kramar-eu-compliance', false, [], KRAMAR_VERSION ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- version constant passed.
			$handle = 'kramar-eu-compliance';
			wp_enqueue_style( 'kramar-eu-compliance' );
		}

		wp_add_inline_style(
			$handle,
			'.kramar-unit-price{display:block;font-size:.85em;color:#555;margin:.2em 0;}' .
			'.kramar-delivery-time{display:block;font-size:.875em;color:#444;margin:.25em 0;}' .
			'.kramar-price-notice{display:block;font-size:.8em;color:#777;margin:.15em 0;}'
		);

		// JS only needed on single product pages with variable products.
		if ( ! is_singular( 'product' ) ) {
			return;
		}

		global $product;
		if ( ! $product instanceof \WC_Product || ! $product->is_type( 'variable' ) ) {
			return;
		}

		// WooCommerce dispatches `found_variation`/`reset_data` via jQuery
		// (`jQuery( form ).trigger()`), which a native addEventListener never
		// receives. The variations form is the one place WC guarantees jQuery, so
		// we bind through it. jQuery passes the matched variation as the 2nd arg.
		wp_add_inline_script(
			'woocommerce',
			'(function(){' .
			'if(!window.jQuery)return;' .
			'jQuery(function($){' .
			'var $form=$("form.variations_form");' .
			'if(!$form.length)return;' .
			'var clear=function(){' .
			'var spans=document.querySelectorAll(".kramar-unit-price");' .
			'for(var i=0;i<spans.length;i++){spans[i].innerHTML="";}' .
			'};' .
			'$form.on("found_variation",function(event,variation){' .
			'if(!variation)return;' .
			'var spans=document.querySelectorAll(".kramar-unit-price");' .
			'var val=variation.kramar_unit_price||"";' .
			'for(var i=0;i<spans.length;i++){spans[i].innerHTML=val;}' .
			'});' .
			'$form.on("reset_data hide_variation",clear);' .
			'});' .
			'}());'
		);
	}

	// -------------------------------------------------------------------------
	// Delivery time: frontend display
	// -------------------------------------------------------------------------

	/**
	 * Output the delivery time span on product page and shop loop.
	 *
	 * Resolution order: taxonomy term assigned to product → global default setting.
	 *
	 * @since 1.0.0
	 */
	public function display_delivery_time(): void {
		global $product;

		if ( ! $product instanceof \WC_Product ) {
			return;
		}

		$label = $this->resolve_delivery_time( $product->get_id() );

		if ( '' === $label ) {
			return;
		}

		$html = sprintf(
			'<span class="kramar-delivery-time">%s</span>',
			esc_html( $label )
		);

		/**
		 * Filter the delivery-time markup before it is printed.
		 *
		 * Lets a theme swap the wrapper (e.g. a badge), while the label itself
		 * can be filtered via `kramar_delivery_time_label`.
		 *
		 * @since 1.0.0
		 *
		 * @param string      $html    The default `<span>` markup (already escaped).
		 * @param string      $label   The resolved delivery-time label.
		 * @param \WC_Product $product The product being displayed.
		 */
		echo wp_kses_post( apply_filters( 'kramar_delivery_time_html', $html, $label, $product ) );
	}

	/**
	 * Resolve the delivery time label for a product.
	 *
	 * Resolution order: the product's own assigned term → the global default
	 * term → empty. This is what makes every product compliant by default (set
	 * the global default once) while still allowing per-product overrides.
	 *
	 * @since 1.0.0
	 *
	 * @param int $product_id WooCommerce product ID.
	 * @return string Delivery time label, or empty string when none is set.
	 */
	public function resolve_delivery_time( int $product_id ): string {
		$label = '';

		$terms = get_the_terms( $product_id, self::DELIVERY_TAX );
		if ( is_array( $terms ) && ! empty( $terms ) ) {
			$term  = reset( $terms );
			$label = $term instanceof \WP_Term ? $term->name : '';
		} else {
			// Fall back to the shop-wide default term. Always stored as a term ID
			// (0 = none set); a stale/deleted ID resolves to '' rather than a label.
			$default_id = (int) $this->get_module_setting( 'delivery_time_default' );
			if ( $default_id > 0 ) {
				$term  = get_term( $default_id, self::DELIVERY_TAX );
				$label = $term instanceof \WP_Term ? $term->name : '';
			}
		}

		/**
		 * Filter the resolved delivery-time label for a product.
		 *
		 * @since 1.0.0
		 *
		 * @param string $label      The resolved label (term name / default / '').
		 * @param int    $product_id The product ID.
		 */
		return (string) apply_filters( 'kramar_delivery_time_label', $label, $product_id );
	}

	// -------------------------------------------------------------------------
	// Tax / shipping notice
	// -------------------------------------------------------------------------

	/**
	 * Append tax & shipping notice to WooCommerce price HTML.
	 *
	 * Only fires in shop / single product context (not cart, checkout, or admin).
	 * The `{tax}` placeholder is omitted when tax is not enabled in WooCommerce.
	 * The `{shipping}` placeholder links to the configured shipping page URL.
	 *
	 * @since 1.0.0
	 *
	 * @param string      $price_html Original price HTML.
	 * @param \WC_Product $product    Product object.
	 * @return string Modified price HTML with notice appended.
	 */
	public function append_price_notice( string $price_html, \WC_Product $product ): string {
		// Skip non-frontend contexts.
		if ( is_admin() ) {
			return $price_html;
		}

		// Skip cart, checkout, emails — only show on shop/product pages.
		if (
			( function_exists( 'is_cart' ) && is_cart() ) ||
			( function_exists( 'is_checkout' ) && is_checkout() )
		) {
			return $price_html;
		}

		// Skip products with no rendered price — appending "incl. VAT | plus
		// shipping" with no amount is misleading and visually broken. See G16.
		if ( '' === trim( wp_strip_all_tags( $price_html ) ) ) {
			return $price_html;
		}

		$template  = (string) $this->get_module_setting( 'tax_notice_text' );
		$tax_text  = '';
		$ship_text = (string) $this->get_module_setting( 'tax_notice_ship_text' );
		$ship_url  = (string) $this->get_module_setting( 'tax_notice_ship_url' );

		// Build the {tax} replacement.
		if ( wc_tax_enabled() ) {
			if ( wc_prices_include_tax() ) {
				$tax_label = (string) $this->get_module_setting( 'tax_notice_tax_text' );
			} else {
				$tax_label = (string) $this->get_module_setting( 'tax_notice_excl_text' );
			}
			$tax_text = esc_html( $tax_label );
		}

		// Build the {shipping} replacement.
		$ship_replacement = '';
		if ( '' !== $ship_text ) {
			if ( '' !== $ship_url ) {
				$ship_replacement = sprintf(
					'<a href="%s">%s</a>',
					esc_url( $ship_url ),
					esc_html( $ship_text )
				);
			} else {
				$ship_replacement = esc_html( $ship_text );
			}
		}

		$notice = str_replace(
			[ '{tax}', '{shipping}' ],
			[ $tax_text, $ship_replacement ],
			$template
		);

		// Remove empty separators (e.g. " | " when tax is disabled).
		$notice = preg_replace( '/^[\s|]+|[\s|]+$/u', '', $notice ) ?? $notice;
		$notice = preg_replace( '/\|\s*\|/u', '|', $notice ) ?? $notice;
		// Clean leading/trailing pipes after replacement.
		$notice = trim( $notice, " \t\n\r\0\x0B|" );

		if ( '' === $notice ) {
			return $price_html;
		}

		return $price_html . sprintf(
			'<small class="kramar-price-notice">%s</small>',
			// Admin-set template; placeholders are pre-escaped and the {shipping}
			// anchor is intentionally allowed. Filter as a defense-in-depth default.
			wp_kses_post( $notice )
		);
	}

	// -------------------------------------------------------------------------
	// Admin settings UI
	// -------------------------------------------------------------------------

	/**
	 * {@inheritDoc}
	 */
	public function render_extra_settings( string $option_name ): void {
		$slug = $this->get_slug();
		$cat  = $this->get_category();
		$n    = $option_name . '[' . $cat . '][' . $slug . ']';

		$unit_price_enabled    = $this->s( 'unit_price_enabled' );
		$delivery_time_enabled = $this->s( 'delivery_time_enabled' );
		$delivery_default      = (int) $this->get_module_setting( 'delivery_time_default' );
		$delivery_time_terms   = $this->get_delivery_time_options();
		$tax_notice_enabled    = $this->s( 'tax_notice_enabled' );
		$tax_notice_text       = (string) $this->get_module_setting( 'tax_notice_text' );
		$tax_notice_tax_text   = (string) $this->get_module_setting( 'tax_notice_tax_text' );
		$tax_notice_excl_text  = (string) $this->get_module_setting( 'tax_notice_excl_text' );
		$tax_notice_ship_text  = (string) $this->get_module_setting( 'tax_notice_ship_text' );
		$tax_notice_ship_url   = (string) $this->get_module_setting( 'tax_notice_ship_url' );
		?>
		<div class="kramar-module__extra">
			<div class="kramar-settings-stack">

				<?php /* ---- Unit price ---- */ ?>
				<p><strong><?php esc_html_e( 'Unit Price (Grundpreis)', 'kramar' ); ?></strong></p>
				<p>
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $n ); ?>[unit_price_enabled]"
							value="1"
							<?php checked( $unit_price_enabled ); ?>
						/>
						<?php esc_html_e( 'Show unit price on product and shop pages', 'kramar' ); ?>
					</label>
					<span class="description"><?php esc_html_e( 'Required by EU/DE Preisangabenverordnung (PAngV). Set content, unit, base quantity and base unit per product in the General tab.', 'kramar' ); ?></span>
				</p>

				<?php /* ---- Delivery time ---- */ ?>
				<p><strong><?php esc_html_e( 'Delivery Time', 'kramar' ); ?></strong></p>
				<p>
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $n ); ?>[delivery_time_enabled]"
							value="1"
							<?php checked( $delivery_time_enabled ); ?>
						/>
						<?php esc_html_e( 'Show delivery time on product and shop pages', 'kramar' ); ?>
					</label>
				</p>
				<p style="padding-left:24px;">
					<label>
						<?php esc_html_e( 'Global default delivery time:', 'kramar' ); ?>
						<select name="<?php echo esc_attr( $n ); ?>[delivery_time_default]">
							<option value="0"><?php esc_html_e( '— None —', 'kramar' ); ?></option>
							<?php foreach ( $delivery_time_terms as $term_id => $term_name ) : ?>
								<option value="<?php echo esc_attr( (string) $term_id ); ?>" <?php selected( $delivery_default, $term_id ); ?>>
									<?php echo esc_html( $term_name ); ?>
								</option>
							<?php endforeach; ?>
						</select>
					</label>
					<span class="description">
						<?php
						if ( empty( $delivery_time_terms ) ) {
							esc_html_e( 'Define your tiers first under Products → Delivery Times, then pick one here as the shop-wide default.', 'kramar' );
						} else {
							esc_html_e( 'Applied to every product that has no delivery time of its own — so the whole catalogue stays compliant by default. Override individual products in Product data → Shipping.', 'kramar' );
						}
						?>
					</span>
				</p>

				<?php /* ---- Delivery time: how to use ---- */ ?>
				<div class="kramar-guide" style="padding-left:24px;margin:8px 0 4px;">
					<p style="margin:0 0 4px;"><em><?php esc_html_e( 'How it works', 'kramar' ); ?></em></p>
					<ol style="margin:0 0 8px 18px;">
						<li><?php esc_html_e( 'Define your delivery tiers once under Products → Delivery Times (e.g. “In stock”, “5–10 working days”, “Made to order”). The term description can hold the longer explanation.', 'kramar' ); ?></li>
						<li><?php esc_html_e( 'Pick one as the Global default above — every product without its own tier shows it (pre-purchase delivery estimates are a consumer-law transparency requirement).', 'kramar' ); ?></li>
						<li><?php esc_html_e( 'Override a specific product in Product data → Shipping → Delivery time. It’s a single choice, decoupled from stock status.', 'kramar' ); ?></li>
					</ol>
					<p style="margin:0 0 4px;"><em><?php esc_html_e( 'For theme developers', 'kramar' ); ?></em></p>
					<ul style="margin:0 0 0 18px;list-style:disc;">
						<li>
							<?php
							printf(
								/* translators: %s: PHP function name, in a <code> tag. */
								wp_kses( __( '%s — returns the resolved label (product tier → global default → empty).', 'kramar' ), [ 'code' => [] ] ),
								'<code>kramar_get_delivery_time( $product_id )</code>'
							);
							?>
						</li>
						<li>
							<?php
							printf(
								/* translators: 1: single-product hook name, 2: shop-loop hook name, both in <code> tags. */
								wp_kses( __( 'Auto-prints inside %1$s (priority 27) and %2$s (priority 12) — unhook to reposition.', 'kramar' ), [ 'code' => [] ] ),
								'<code>woocommerce_single_product_summary</code>',
								'<code>woocommerce_after_shop_loop_item_title</code>'
							);
							?>
						</li>
						<li>
							<?php
							printf(
								/* translators: 1: label filter, 2: html filter, 3: CSS class — all in <code> tags. */
								wp_kses( __( 'Filters: %1$s and %2$s. Style via the %3$s class.', 'kramar' ), [ 'code' => [] ] ),
								'<code>kramar_delivery_time_label</code>',
								'<code>kramar_delivery_time_html</code>',
								'<code>.kramar-delivery-time</code>'
							);
							?>
						</li>
					</ul>
				</div>

				<?php /* ---- Tax / shipping notice ---- */ ?>
				<p><strong><?php esc_html_e( 'Tax &amp; Shipping Notice', 'kramar' ); ?></strong></p>
				<p>
					<label>
						<input
							type="checkbox"
							name="<?php echo esc_attr( $n ); ?>[tax_notice_enabled]"
							value="1"
							<?php checked( $tax_notice_enabled ); ?>
						/>
						<?php esc_html_e( 'Append tax &amp; shipping notice to price HTML on shop/product pages', 'kramar' ); ?>
					</label>
				</p>
				<p style="padding-left:24px;">
					<label>
						<?php esc_html_e( 'Notice template:', 'kramar' ); ?>
						<input
							type="text"
							name="<?php echo esc_attr( $n ); ?>[tax_notice_text]"
							value="<?php echo esc_attr( $tax_notice_text ); ?>"
							class="regular-text"
						/>
					</label>
					<span class="description">
						<?php
						/* translators: explaining {tax} and {shipping} placeholders in the notice template field */
						esc_html_e( 'Placeholders: {tax} (tax label), {shipping} (shipping link/text). Empty placeholders are trimmed.', 'kramar' );
						?>
					</span>
				</p>
				<p style="padding-left:24px;">
					<label>
						<?php esc_html_e( 'Tax included label:', 'kramar' ); ?>
						<input
							type="text"
							name="<?php echo esc_attr( $n ); ?>[tax_notice_tax_text]"
							value="<?php echo esc_attr( $tax_notice_tax_text ); ?>"
							class="regular-text"
						/>
					</label>
				</p>
				<p style="padding-left:24px;">
					<label>
						<?php esc_html_e( 'Tax excluded label:', 'kramar' ); ?>
						<input
							type="text"
							name="<?php echo esc_attr( $n ); ?>[tax_notice_excl_text]"
							value="<?php echo esc_attr( $tax_notice_excl_text ); ?>"
							class="regular-text"
						/>
					</label>
				</p>
				<p style="padding-left:24px;">
					<label>
						<?php esc_html_e( 'Shipping text:', 'kramar' ); ?>
						<input
							type="text"
							name="<?php echo esc_attr( $n ); ?>[tax_notice_ship_text]"
							value="<?php echo esc_attr( $tax_notice_ship_text ); ?>"
							class="regular-text"
							placeholder="<?php esc_attr_e( 'e.g. plus shipping', 'kramar' ); ?>"
						/>
					</label>
				</p>
				<p style="padding-left:24px;">
					<label>
						<?php esc_html_e( 'Shipping page URL (optional):', 'kramar' ); ?>
						<input
							type="url"
							name="<?php echo esc_attr( $n ); ?>[tax_notice_ship_url]"
							value="<?php echo esc_attr( $tax_notice_ship_url ); ?>"
							class="regular-text"
						/>
					</label>
					<span class="description"><?php esc_html_e( 'If set, the shipping text becomes a link.', 'kramar' ); ?></span>
				</p>

			</div>
		</div>
		<?php
	}

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

	/**
	 * Boolean shorthand for a module setting.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key Setting key.
	 * @return bool Truthy value of the setting.
	 */
	private function s( string $key ): bool {
		return (bool) $this->get_module_setting( $key );
	}

	/**
	 * Get a setting for this module.
	 *
	 * @since 1.0.0
	 *
	 * @param string $key Setting key.
	 * @return mixed Setting value.
	 */
	private function get_module_setting( string $key ): mixed {
		return $this->plugin->get_setting( $this->get_category(), $this->get_slug(), $key );
	}
}
