<?php
/**
 * Product Extra Options module.
 *
 * @package Kramar
 * @since   1.0.0
 */

declare( strict_types=1 );

namespace Kramar\Modules\Products;

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

/**
 * Product Extra Options module.
 *
 * Adds per-product custom fields (text, textarea, select, checkbox, radio, number)
 * with optional price surcharges. Replaces WooCommerce Product Add-ons.
 *
 * @since 1.0.0
 */
final class Extra_Options extends \Kramar\Module {

	/**
	 * Product meta key for storing extra option configs.
	 *
	 * @since 1.0.0
	 */
	private const META_KEY = '_kramar_extra_options';

	/**
	 * Cart item data key for storing selected extra options.
	 *
	 * @since 1.0.0
	 */
	private const CART_KEY = '_kramar_extra_options_data';

	/**
	 * Untouched cart item prices for this request, keyed by cart item key.
	 *
	 * Only meaningful within a single request: the cart is rebuilt from the
	 * session on the next one, so the prices are re-read then.
	 *
	 * @since 1.0.2
	 * @var array<string, float>
	 */
	private array $base_prices = [];

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

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

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

	/**
	 * {@inheritDoc}
	 */
	public function get_description(): string {
		return __( 'Add per-product custom fields (text, select, checkbox, radio, number) with optional price surcharges. Configured per product on the product edit screen.', 'kramar' );
	}

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

	/**
	 * {@inheritDoc}
	 */
	public function sanitize_settings( array $input ): array {
		return [
			'enabled' => ! empty( $input['enabled'] ),
		];
	}

	/**
	 * {@inheritDoc}
	 */
	protected function run(): void {
		// Admin: meta box on product edit screen.
		add_action( 'add_meta_boxes', [ $this, 'register_meta_box' ] );
		add_action( 'woocommerce_process_product_meta', [ $this, 'save_meta_box' ] );
		add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_product_assets' ] );

		// Frontend: render fields before add-to-cart button.
		add_action( 'woocommerce_before_add_to_cart_button', [ $this, 'render_frontend_fields' ] );

		// Shortcode fallback for themes with fully custom product templates.
		add_shortcode( 'kramar_product_options', [ $this, 'shortcode_product_options' ] );

		// Cart: validation, data persistence, price adjustment, display.
		add_filter( 'woocommerce_add_to_cart_validation', [ $this, 'validate_cart_item' ], 10, 3 );
		add_filter( 'woocommerce_add_cart_item_data', [ $this, 'add_cart_item_data' ], 10, 2 );
		add_filter( 'woocommerce_get_item_data', [ $this, 'display_cart_item_data' ], 10, 2 );
		add_action( 'woocommerce_before_calculate_totals', [ $this, 'adjust_cart_item_prices' ] );

		// Order: persist to order line items.
		add_action( 'woocommerce_checkout_create_order_line_item', [ $this, 'save_order_item_meta' ], 10, 4 );
	}

	// -------------------------------------------------------------------------
	// Admin Meta Box
	// -------------------------------------------------------------------------

	/**
	 * Register the meta box on the product edit screen.
	 *
	 * @since 1.0.0
	 */
	public function register_meta_box(): void {
		add_meta_box(
			'kramar-extra-options',
			__( 'Kramar Extra Options', 'kramar' ),
			[ $this, 'render_meta_box' ],
			'product',
			'normal',
			'default'
		);
	}

	/**
	 * Render the meta box content.
	 *
	 * @since 1.0.0
	 *
	 * @param \WP_Post $post Current product post.
	 */
	public function render_meta_box( \WP_Post $post ): void {
		wp_nonce_field( 'kramar_extra_options_nonce', '_kramar_extra_options_nonce' );

		$raw     = get_post_meta( $post->ID, self::META_KEY, true );
		$options = [];
		if ( is_string( $raw ) && '' !== $raw ) {
			$decoded = json_decode( $raw, true );
			if ( is_array( $decoded ) ) {
				$options = $decoded;
			}
		}

		$types = [
			'text'     => __( 'Text', 'kramar' ),
			'textarea' => __( 'Textarea', 'kramar' ),
			'select'   => __( 'Select', 'kramar' ),
			'checkbox' => __( 'Checkbox', 'kramar' ),
			'radio'    => __( 'Radio', 'kramar' ),
			'number'   => __( 'Number', 'kramar' ),
		];
		?>
		<div id="kramar-extra-options-wrap" style="margin:12px 0;overflow-x:auto;">
			<table class="widefat" id="kramar-eo-table" style="table-layout:fixed;margin-bottom:12px;min-width:960px;">
				<thead>
					<tr>
						<th style="width:160px;"><?php esc_html_e( 'Key', 'kramar' ); ?></th>
						<th style="width:100px;"><?php esc_html_e( 'Type', 'kramar' ); ?></th>
						<th style="width:200px;"><?php esc_html_e( 'Label', 'kramar' ); ?></th>
						<th style="width:70px;"><?php esc_html_e( 'Required', 'kramar' ); ?></th>
						<th style="width:160px;"><?php esc_html_e( 'Options (pipe-separated)', 'kramar' ); ?></th>
						<th style="width:130px;"><?php esc_html_e( 'Prices (pipe-separated)', 'kramar' ); ?></th>
						<th style="width:80px;"><?php esc_html_e( 'Price (single)', 'kramar' ); ?></th>
						<th style="width:60px;"></th>
					</tr>
				</thead>
				<tbody id="kramar-eo-rows">
					<?php foreach ( $options as $i => $opt ) : ?>
						<?php $this->render_meta_box_row( $i, $opt, $types ); ?>
					<?php endforeach; ?>
				</tbody>
			</table>

			<button type="button" id="kramar-eo-add" class="button">
				<?php esc_html_e( '+ Add Option', 'kramar' ); ?>
			</button>

			<script type="text/template" id="kramar-eo-row-template">
				<?php $this->render_meta_box_row( '__INDEX__', [], $types ); ?>
			</script>
		</div>
		<?php
	}

	/**
	 * Render a single repeatable meta box row.
	 *
	 * @since 1.0.0
	 *
	 * @param int|string           $index  Row index (or '__INDEX__' for template).
	 * @param array<string, mixed> $opt    Option data.
	 * @param array<string, string> $types Available field types.
	 */
	private function render_meta_box_row( int|string $index, array $opt, array $types ): void {
		$key      = isset( $opt['key'] ) ? esc_attr( (string) $opt['key'] ) : '';
		$type     = isset( $opt['type'] ) ? esc_attr( (string) $opt['type'] ) : 'text';
		$label    = isset( $opt['label'] ) ? esc_attr( (string) $opt['label'] ) : '';
		$required = ! empty( $opt['required'] );
		$options  = isset( $opt['options'] ) ? esc_attr( (string) $opt['options'] ) : '';
		$prices   = isset( $opt['prices'] ) ? esc_attr( (string) $opt['prices'] ) : '';
		$price    = isset( $opt['price'] ) ? esc_attr( (string) $opt['price'] ) : '';
		?>
		<tr class="kramar-eo-row">
			<td>
				<input
					type="text"
					name="kramar_eo[<?php echo esc_attr( (string) $index ); ?>][key]"
					value="<?php echo esc_attr( $key ); ?>"
					placeholder="gift_wrapping"
					class="regular-text"
					style="width:100%;"
				/>
			</td>
			<td>
				<select name="kramar_eo[<?php echo esc_attr( (string) $index ); ?>][type]" style="width:100%;">
					<?php foreach ( $types as $val => $lbl ) : ?>
						<option value="<?php echo esc_attr( $val ); ?>" <?php selected( $type, $val ); ?>>
							<?php echo esc_html( $lbl ); ?>
						</option>
					<?php endforeach; ?>
				</select>
			</td>
			<td>
				<input
					type="text"
					name="kramar_eo[<?php echo esc_attr( (string) $index ); ?>][label]"
					value="<?php echo esc_attr( $label ); ?>"
					placeholder="<?php esc_attr_e( 'Gift wrapping', 'kramar' ); ?>"
					style="width:100%;"
				/>
			</td>
			<td style="text-align:center;">
				<input
					type="checkbox"
					name="kramar_eo[<?php echo esc_attr( (string) $index ); ?>][required]"
					value="1"
					<?php checked( $required ); ?>
				/>
			</td>
			<td>
				<input
					type="text"
					name="kramar_eo[<?php echo esc_attr( (string) $index ); ?>][options]"
					value="<?php echo esc_attr( $options ); ?>"
					placeholder="Small|Medium|Large"
					style="width:100%;"
				/>
			</td>
			<td>
				<input
					type="text"
					name="kramar_eo[<?php echo esc_attr( (string) $index ); ?>][prices]"
					value="<?php echo esc_attr( $prices ); ?>"
					placeholder="0|2.50|5.00"
					style="width:100%;"
				/>
			</td>
			<td>
				<input
					type="number"
					name="kramar_eo[<?php echo esc_attr( (string) $index ); ?>][price]"
					value="<?php echo esc_attr( $price ); ?>"
					step="0.01"
					min="0"
					placeholder="2.50"
					style="width:100%;"
				/>
			</td>
			<td>
				<button type="button" class="button kramar-eo-remove" style="color:#a00;">
					<?php esc_html_e( 'Remove', 'kramar' ); ?>
				</button>
			</td>
		</tr>
		<?php
	}

	/**
	 * Save extra options from the meta box.
	 *
	 * @since 1.0.0
	 *
	 * @param int $product_id Product post ID.
	 */
	public function save_meta_box( int $product_id ): void {
		if ( ! isset( $_POST['_kramar_extra_options_nonce'] ) ) {
			return;
		}
		if ( ! wp_verify_nonce( sanitize_key( $_POST['_kramar_extra_options_nonce'] ), 'kramar_extra_options_nonce' ) ) {
			return;
		}
		if ( ! current_user_can( 'edit_post', $product_id ) ) {
			return;
		}

		$raw_rows = isset( $_POST['kramar_eo'] ) && is_array( $_POST['kramar_eo'] ) ? $_POST['kramar_eo'] : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput

		$options      = [];
		$allowed_types = [ 'text', 'textarea', 'select', 'checkbox', 'radio', 'number' ];

		foreach ( $raw_rows as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}

			$key = sanitize_key( $row['key'] ?? '' );
			if ( '' === $key ) {
				continue;
			}

			$type = sanitize_key( $row['type'] ?? 'text' );
			if ( ! in_array( $type, $allowed_types, true ) ) {
				$type = 'text';
			}

			$options[] = [
				'key'      => $key,
				'type'     => $type,
				'label'    => sanitize_text_field( $row['label'] ?? '' ),
				'required' => ! empty( $row['required'] ),
				'options'  => sanitize_text_field( $row['options'] ?? '' ),
				'prices'   => sanitize_text_field( $row['prices'] ?? '' ),
				'price'    => (float) ( $row['price'] ?? 0 ),
			];
		}

		$json = wp_json_encode( $options );
		if ( false === $json ) {
			return;
		}

		// wp_slash: update_post_meta() runs wp_unslash() on the value, which would
		// strip the escaping wp_json_encode() adds — a \" from a quote in an option
		// label becomes ", corrupting the JSON so it can no longer be decoded on
		// read, and every option on the product silently disappears. An option
		// labelled 'Engraving "Deluxe"' is enough to trigger it.
		update_post_meta( $product_id, self::META_KEY, wp_slash( $json ) );
	}

	/**
	 * Enqueue the repeater JS on the product edit screen.
	 *
	 * @since 1.0.0
	 */
	public function enqueue_product_assets(): void {
		$screen = get_current_screen();
		if ( ! $screen || 'product' !== $screen->id ) {
			return;
		}
		wp_enqueue_script(
			'kramar-extra-options-admin',
			KRAMAR_URL . 'assets/js/extra-options-admin.js',
			[],
			KRAMAR_VERSION,
			true
		);
	}

	// -------------------------------------------------------------------------
	// Frontend Rendering
	// -------------------------------------------------------------------------

	/**
	 * Render extra option fields on the single product page.
	 *
	 * @since 1.0.0
	 */
	public function shortcode_product_options(): string {
		ob_start();
		$this->render_frontend_fields();
		return (string) ob_get_clean();
	}

	public function render_frontend_fields(): void {
		global $product;

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

		$options = $this->get_product_options( $product->get_id() );
		if ( empty( $options ) ) {
			return;
		}

		echo '<div class="kramar-extra-options">';

		foreach ( $options as $opt ) {
			$key      = (string) $opt['key'];
			$label    = (string) $opt['label'];
			$type     = $opt['type'];
			$required = $opt['required'];
			$field_id = 'kramar_eo_' . sanitize_key( $key );

			echo '<div class="kramar-extra-option kramar-extra-option--' . esc_attr( $type ) . '" style="margin-bottom:12px;">';

			switch ( $type ) {
				case 'checkbox':
					$price_display = $this->format_surcharge( $opt['price'] );
					printf(
						'<label for="%s"><input type="checkbox" id="%s" name="kramar_eo[%s]" value="1"%s /> %s%s</label>',
						esc_attr( $field_id ),
						esc_attr( $field_id ),
						esc_attr( $key ),
						$required ? ' required' : '',
						esc_html( $label ),
						'' !== $price_display ? ' <span class="kramar-eo-surcharge">(' . esc_html( $price_display ) . ')</span>' : ''
					);
					break;

				case 'select':
					$raw_opts   = self::split_options( (string) $opt['options'] );
					$raw_prices = array_map( 'trim', explode( '|', $opt['prices'] ) );

					printf(
						'<label for="%s">%s%s</label>',
						esc_attr( $field_id ),
						esc_html( $label ),
						$required ? ' <span class="required">*</span>' : ''
					);
					printf(
						'<select id="%s" name="kramar_eo[%s]"%s style="display:block;margin-top:4px;">',
						esc_attr( $field_id ),
						esc_attr( $key ),
						$required ? ' required' : ''
					);
					echo '<option value="">' . esc_html__( '-- Select --', 'kramar' ) . '</option>';
					foreach ( $raw_opts as $idx => $choice ) {
						$choice_price   = isset( $raw_prices[ $idx ] ) ? (float) $raw_prices[ $idx ] : 0.0;
						$price_display  = $this->format_surcharge( $choice_price );
						$display_label  = esc_html( $choice );
						if ( '' !== $price_display ) {
							$display_label .= ' (' . esc_html( $price_display ) . ')';
						}
						printf(
							'<option value="%s">%s</option>',
							esc_attr( $choice ),
							$display_label // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- assembled from esc_html() parts above.
						);
					}
					echo '</select>';
					break;

				case 'radio':
					$raw_opts   = self::split_options( (string) $opt['options'] );
					$raw_prices = array_map( 'trim', explode( '|', $opt['prices'] ) );

					echo '<fieldset style="border:none;padding:0;margin:0;">';
					printf(
						'<legend>%s%s</legend>',
						esc_html( $label ),
						$required ? ' <span class="required">*</span>' : ''
					);
					foreach ( $raw_opts as $idx => $choice ) {
						$choice_price  = isset( $raw_prices[ $idx ] ) ? (float) $raw_prices[ $idx ] : 0.0;
						$price_display = $this->format_surcharge( $choice_price );
						$radio_id      = $field_id . '_' . sanitize_key( $choice );
						printf(
							'<label for="%s" style="display:block;"><input type="radio" id="%s" name="kramar_eo[%s]" value="%s"%s /> %s%s</label>',
							esc_attr( $radio_id ),
							esc_attr( $radio_id ),
							esc_attr( $key ),
							esc_attr( $choice ),
							$required ? ' required' : '',
							esc_html( $choice ),
							'' !== $price_display ? ' <span class="kramar-eo-surcharge">(' . esc_html( $price_display ) . ')</span>' : ''
						);
					}
					echo '</fieldset>';
					break;

				case 'textarea':
					printf(
						'<label for="%s">%s%s</label>',
						esc_attr( $field_id ),
						esc_html( $label ),
						$required ? ' <span class="required">*</span>' : ''
					);
					printf(
						'<textarea id="%s" name="kramar_eo[%s]"%s style="display:block;width:100%%;margin-top:4px;" rows="3"></textarea>',
						esc_attr( $field_id ),
						esc_attr( $key ),
						$required ? ' required' : ''
					);
					break;

				case 'number':
					$price_display = $this->format_surcharge( $opt['price'] );
					printf(
						'<label for="%s">%s%s%s</label>',
						esc_attr( $field_id ),
						esc_html( $label ),
						$required ? ' <span class="required">*</span>' : '',
						'' !== $price_display ? ' <span class="kramar-eo-surcharge">(' . esc_html( $price_display ) . ')</span>' : ''
					);
					printf(
						'<input type="number" id="%s" name="kramar_eo[%s]"%s style="display:block;margin-top:4px;" min="0" step="1" />',
						esc_attr( $field_id ),
						esc_attr( $key ),
						$required ? ' required' : ''
					);
					break;

				default: // text
					$price_display = $this->format_surcharge( $opt['price'] );
					printf(
						'<label for="%s">%s%s%s</label>',
						esc_attr( $field_id ),
						esc_html( $label ),
						$required ? ' <span class="required">*</span>' : '',
						'' !== $price_display ? ' <span class="kramar-eo-surcharge">(' . esc_html( $price_display ) . ')</span>' : ''
					);
					printf(
						'<input type="text" id="%s" name="kramar_eo[%s]"%s style="display:block;width:100%%;margin-top:4px;" />',
						esc_attr( $field_id ),
						esc_attr( $key ),
						$required ? ' required' : ''
					);
					break;
			}

			echo '</div>';
		}

		echo '</div>';
	}

	// -------------------------------------------------------------------------
	// Cart: Validation
	// -------------------------------------------------------------------------

	/**
	 * Validate that required extra options are filled before adding to cart.
	 *
	 * @since 1.0.0
	 *
	 * @param bool $passed     Whether validation passed so far.
	 * @param int  $product_id Product ID.
	 * @param int  $quantity   Quantity being added.
	 * @return bool Whether validation passed.
	 */
	public function validate_cart_item( bool $passed, int $product_id, int $quantity ): bool {
		$options = $this->get_product_options( $product_id );

		foreach ( $options as $opt ) {
			if ( ! $opt['required'] ) {
				continue;
			}

			$key   = $opt['key'];
			// phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- add-to-cart option read during WC cart validation (WC handles the cart nonce); value is validated against the allowed choices below.
			$value = isset( $_POST['kramar_eo'][ $key ] ) ? trim( (string) $_POST['kramar_eo'][ $key ] ) : '';

			if ( '' === $value || '0' === $value ) {
				wc_add_notice(
					/* translators: %s: option label */
					sprintf( __( 'Please fill in the required field: %s', 'kramar' ), esc_html( $opt['label'] ) ),
					'error'
				);
				$passed = false;
			}
		}

		// Choice fields have to carry one of the configured choices. Nothing
		// checked this before, and calculate_surcharge() charges 0 for a value it
		// doesn't recognise — so posting an invented option value bought the
		// upgrade for free while the fabricated text was stored on the cart, the
		// order and the admin as though it were a real choice.
		foreach ( $options as $opt ) {
			if ( ! in_array( $opt['type'], [ 'select', 'radio' ], true ) ) {
				continue;
			}

			// phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- same add-to-cart POST as above; compared against the configured allow-list, never used as-is.
			$value = isset( $_POST['kramar_eo'][ $opt['key'] ] ) ? trim( (string) $_POST['kramar_eo'][ $opt['key'] ] ) : '';
			if ( '' === $value ) {
				continue; // absent is handled by the required check above
			}

			$allowed = array_values( self::split_options( (string) ( $opt['options'] ?? '' ) ) );
			if ( ! in_array( $value, $allowed, true ) ) {
				wc_add_notice(
					/* translators: %s: option label */
					sprintf( __( 'Please choose a valid option for: %s', 'kramar' ), esc_html( $opt['label'] ) ),
					'error'
				);
				$passed = false;
			}
		}

		return $passed;
	}

	// -------------------------------------------------------------------------
	// Cart: Persistence
	// -------------------------------------------------------------------------

	/**
	 * Store selected extra option values in cart item data.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, mixed> $cart_item_data Existing cart item data.
	 * @param int                  $product_id     Product ID.
	 * @return array<string, mixed> Extended cart item data.
	 */
	public function add_cart_item_data( array $cart_item_data, int $product_id ): array {
		$options = $this->get_product_options( $product_id );
		if ( empty( $options ) ) {
			return $cart_item_data;
		}

		$selected = [];

		foreach ( $options as $opt ) {
			$key = $opt['key'];

			if ( ! isset( $_POST['kramar_eo'][ $key ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
				continue;
			}

			$raw_value = $_POST['kramar_eo'][ $key ]; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput,WordPress.Security.NonceVerification

			if ( 'textarea' === $opt['type'] ) {
				$value = sanitize_textarea_field( (string) $raw_value );
			} elseif ( 'number' === $opt['type'] ) {
				$value = (string) absint( $raw_value );
			} elseif ( 'checkbox' === $opt['type'] ) {
				$value = '1' === (string) $raw_value ? '1' : '';
			} else {
				$value = sanitize_text_field( (string) $raw_value );
			}

			if ( '' === $value ) {
				continue;
			}

			$surcharge = $this->calculate_surcharge( $opt, $value );

			$selected[ $key ] = [
				'label'     => $opt['label'],
				'value'     => $value,
				'surcharge' => $surcharge,
			];
		}

		if ( ! empty( $selected ) ) {
			$cart_item_data[ self::CART_KEY ] = $selected;
		}

		return $cart_item_data;
	}

	/**
	 * Display extra option values in cart and checkout review.
	 *
	 * @since 1.0.0
	 *
	 * @param array<int, array<string, string>> $item_data  Existing display data.
	 * @param array<string, mixed>              $cart_item  Cart item.
	 * @return array<int, array<string, string>> Extended display data.
	 */
	public function display_cart_item_data( array $item_data, array $cart_item ): array {
		if ( empty( $cart_item[ self::CART_KEY ] ) || ! is_array( $cart_item[ self::CART_KEY ] ) ) {
			return $item_data;
		}

		foreach ( $cart_item[ self::CART_KEY ] as $entry ) {
			$display_value = esc_html( $entry['value'] );
			if ( $entry['surcharge'] > 0 ) {
				$display_value .= ' (' . $this->format_surcharge( $entry['surcharge'] ) . ')';
			}

			$item_data[] = [
				'name'  => esc_html( $entry['label'] ),
				'value' => $display_value,
			];
		}

		return $item_data;
	}

	// -------------------------------------------------------------------------
	// Cart: Price Adjustment
	// -------------------------------------------------------------------------

	/**
	 * Adjust cart item prices based on extra option surcharges.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Cart $cart The WooCommerce cart.
	 */
	public function adjust_cart_item_prices( \WC_Cart $cart ): void {
		if ( is_admin() && ! defined( 'DOING_AJAX' ) ) {
			return;
		}

		foreach ( $cart->get_cart() as $cart_item_key => $cart_item ) {
			if ( empty( $cart_item[ self::CART_KEY ] ) || ! is_array( $cart_item[ self::CART_KEY ] ) ) {
				continue;
			}

			$extra_cost = 0.0;
			foreach ( $cart_item[ self::CART_KEY ] as $entry ) {
				$extra_cost += (float) $entry['surcharge'];
			}

			if ( $extra_cost <= 0.0 ) {
				continue;
			}

			$product = $cart_item['data'];

			// woocommerce_before_calculate_totals fires SEVERAL times in a normal
			// request — recalculating shipping, applying a coupon, and every
			// Store API call each trigger it. Reading get_price() here would read
			// the price this method already surcharged on the previous pass and
			// add the surcharge again, so an item's price climbed by the
			// surcharge on every recalculation: 104, 109, 114, 119… for a €99
			// product with one €5 option. Remember the untouched price per cart
			// item for the request and always add to that, so running twice
			// produces the same total as running once.
			if ( ! isset( $this->base_prices[ $cart_item_key ] ) ) {
				$this->base_prices[ $cart_item_key ] = (float) $product->get_price();
			}

			$product->set_price( $this->base_prices[ $cart_item_key ] + $extra_cost );
		}
	}

	// -------------------------------------------------------------------------
	// Order: Persist to Line Items
	// -------------------------------------------------------------------------

	/**
	 * Save extra option values as order item meta when checkout creates the order.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order_Item_Product $item          Order line item.
	 * @param string                 $cart_item_key Cart item key.
	 * @param array<string, mixed>   $values        Cart item data.
	 * @param \WC_Order              $order         The order.
	 */
	public function save_order_item_meta(
		\WC_Order_Item_Product $item,
		string $cart_item_key,
		array $values,
		\WC_Order $order
	): void {
		if ( empty( $values[ self::CART_KEY ] ) || ! is_array( $values[ self::CART_KEY ] ) ) {
			return;
		}

		foreach ( $values[ self::CART_KEY ] as $entry ) {
			$display_value = $entry['value'];
			if ( (float) $entry['surcharge'] > 0 ) {
				$display_value .= ' (' . $this->format_surcharge( (float) $entry['surcharge'] ) . ')';
			}

			$item->add_meta_data(
				sanitize_text_field( $entry['label'] ),
				sanitize_text_field( $display_value ),
				false
			);
		}
	}

	// -------------------------------------------------------------------------
	// Admin Settings UI
	// -------------------------------------------------------------------------

	/**
	 * {@inheritDoc}
	 */
	public function render_extra_settings( string $option_name ): void {
		?>
		<div class="kramar-module__extra" style="flex-direction:column;align-items:flex-start;">
			<p class="description"><?php esc_html_e( 'Extra options are configured per product. Open any product in the editor and look for the "Kramar Extra Options" meta box.', 'kramar' ); ?></p>
			<p class="description"><strong><?php esc_html_e( 'Custom themes:', 'kramar' ); ?></strong><br /><?php esc_html_e( 'If your theme uses a custom product template, add the WC hook inside your form or use the shortcode:', 'kramar' ); ?></p>
			<p><code>do_action( 'woocommerce_before_add_to_cart_button' );</code></p>
			<p><code>echo do_shortcode( '[kramar_product_options]' );</code></p>
		</div>
		<?php
	}

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

	/**
	 * Load and decode extra options for a given product.
	 *
	 * @since 1.0.0
	 *
	 * @param int $product_id Product ID.
	 * @return array<int, array<string, mixed>> Option configs.
	 */
	private function get_product_options( int $product_id ): array {
		$raw = get_post_meta( $product_id, self::META_KEY, true );
		if ( ! is_string( $raw ) || '' === $raw ) {
			return [];
		}
		$decoded = json_decode( $raw, true );
		return is_array( $decoded ) ? $decoded : [];
	}

	/**
	 * Calculate the price surcharge for a given option and selected value.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, mixed> $opt   Option config.
	 * @param string               $value Selected value.
	 * @return float Surcharge amount.
	 */
	/**
	 * Split a pipe-separated option list, keeping positional keys.
	 *
	 * array_filter() with no callback drops every falsy value, and the string
	 * "0" is falsy in PHP — so an option literally named "0" ("0 extras", a size
	 * 0, a 0-day rush) vanished from the select and the radio group entirely and
	 * could never be chosen. Only blank segments should go.
	 *
	 * Keys are preserved: prices are positional, so reindexing here while another
	 * caller does not is what once made the page advertise one price and the cart
	 * charge another.
	 *
	 * @since 1.0.2
	 *
	 * @param string $raw Pipe-separated list.
	 * @return array<int, string> Option labels keyed by original position.
	 */
	private static function split_options( string $raw ): array {
		$parts = array_map( 'trim', explode( '|', $raw ) );

		$out  = [];
		$seen = [];

		foreach ( $parts as $i => $part ) {
			if ( '' === $part ) {
				continue;
			}

			// A repeated label cannot be offered twice: both render the same option
			// value, the browser submits that value, and the price lookup always
			// resolves to the first match — so picking the second one showed its
			// surcharge and charged the first one's.
			if ( isset( $seen[ $part ] ) ) {
				continue;
			}

			$seen[ $part ] = true;
			$out[ $i ]     = $part;
		}

		return $out;
	}

	private function calculate_surcharge( array $opt, string $value ): float {
		$type = $opt['type'];

		if ( in_array( $type, [ 'select', 'radio' ], true ) ) {
			// Keys are deliberately NOT reindexed: prices are positional, and an
			// empty segment in the options list ("Red||Blue", a stray extra pipe)
			// still occupies a position in the prices list. Reindexing here while
			// the product page did not shifted every later option onto the price
			// before it — the page advertised "Blue + €2,00" and the cart charged
			// €1,00. Whichever way the prices happen to be ordered, the customer
			// is shown one price and charged another.
			$raw_opts   = self::split_options( (string) ( $opt['options'] ?? '' ) );
			$raw_prices = array_map( 'trim', explode( '|', (string) ( $opt['prices'] ?? '' ) ) );

			$idx = array_search( $value, $raw_opts, true );
			if ( false !== $idx && isset( $raw_prices[ $idx ] ) ) {
				return (float) $raw_prices[ $idx ];
			}
			return 0.0;
		}

		// checkbox, text, textarea, number — flat price surcharge.
		return (float) ( $opt['price'] ?? 0 );
	}

	/**
	 * Format a surcharge amount as "+ €X.XX".
	 *
	 * Returns an empty string when the amount is zero or negative.
	 *
	 * @since 1.0.0
	 *
	 * @param float $amount Surcharge amount.
	 * @return string Formatted surcharge or empty string.
	 */
	private function format_surcharge( float $amount ): string {
		if ( $amount <= 0.0 ) {
			return '';
		}

		return '+ ' . get_woocommerce_currency_symbol() . number_format( $amount, 2, ',', '.' );
	}
}
