<?php
/**
 * EU VAT ID / B2B module.
 *
 * @package Kramar
 * @since   1.0.0
 */

declare( strict_types=1 );

namespace Kramar\Modules\Checkout;

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

/**
 * EU VAT ID / B2B module.
 *
 * Adds a VAT ID field to the checkout billing section and validates its
 * format against the per-country EU VAT pattern. A format-valid VAT number
 * can make the customer VAT-exempt (reverse charge) and stores validation
 * evidence on the order for tax records.
 *
 * @since 1.0.0
 */
final class Vat_Id extends \Kramar\Module {

	/**
	 * Minimum length (digits + letters) of the number part per EU country code.
	 *
	 * @since 1.0.0
	 *
	 * @var array<string, int>
	 */
	private const VAT_PATTERNS = [
		'AT' => 9,  'BE' => 10, 'BG' => 9,  'CY' => 9,
		'CZ' => 8,  'DE' => 9,  'DK' => 8,  'EE' => 9,
		'EL' => 9,  'ES' => 9,  'FI' => 8,  'FR' => 11,
		'HR' => 11, 'HU' => 8,  'IE' => 8,  'IT' => 11,
		'LT' => 9,  'LU' => 8,  'LV' => 11, 'MT' => 8,
		'NL' => 12, 'PL' => 10, 'PT' => 9,  'RO' => 2,
		'SE' => 12, 'SI' => 8,  'SK' => 10, 'XI' => 5,
	];

	/**
	 * Per-country VAT number patterns (national part, country prefix removed).
	 *
	 * The length map above only ever established a MINIMUM, so "AAAAAAAAAAAA"
	 * passed as a valid Dutch VAT number and a nineteen-digit string passed too.
	 * Since a valid-looking number can grant a reverse-charge exemption, the
	 * shape has to actually be checked — this is still offline format
	 * validation, not a VIES existence check.
	 *
	 * @since 1.0.2
	 * @var array<string, string>
	 */
	private const VAT_REGEX = [
		'AT' => '/^U[0-9]{8}$/',
		'BE' => '/^[01][0-9]{9}$/',
		'BG' => '/^[0-9]{9,10}$/',
		'CY' => '/^[0-9]{8}[A-Z]$/',
		'CZ' => '/^[0-9]{8,10}$/',
		'DE' => '/^[0-9]{9}$/',
		'DK' => '/^[0-9]{8}$/',
		'EE' => '/^[0-9]{9}$/',
		'EL' => '/^[0-9]{9}$/',
		'ES' => '/^[A-Z0-9][0-9]{7}[A-Z0-9]$/',
		'FI' => '/^[0-9]{8}$/',
		'FR' => '/^[A-Z0-9]{2}[0-9]{9}$/',
		'HR' => '/^[0-9]{11}$/',
		'HU' => '/^[0-9]{8}$/',
		'IE' => '/^([0-9]{7}[A-Z]{1,2}|[0-9][A-Z][0-9]{5}[A-Z])$/',
		'IT' => '/^[0-9]{11}$/',
		'LT' => '/^([0-9]{9}|[0-9]{12})$/',
		'LU' => '/^[0-9]{8}$/',
		'LV' => '/^[0-9]{11}$/',
		'MT' => '/^[0-9]{8}$/',
		'NL' => '/^[0-9]{9}B[0-9]{2}$/',
		'PL' => '/^[0-9]{10}$/',
		'PT' => '/^[0-9]{9}$/',
		'RO' => '/^[0-9]{2,10}$/',
		'SE' => '/^[0-9]{12}$/',
		'SI' => '/^[0-9]{8}$/',
		'SK' => '/^[0-9]{10}$/',
		'XI' => '/^([0-9]{9}|[0-9]{12}|(GD|HA)[0-9]{3})$/',
	];

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

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

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

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

	/**
	 * {@inheritDoc}
	 */
	public function get_description(): string {
		return __( 'Adds a VAT number field to checkout. Validates the VAT number format per EU country and can auto-exempt valid VAT IDs from tax (reverse charge). Stores validation evidence on the order for tax records.', 'kramar' );
	}

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

	/**
	 * {@inheritDoc}
	 */
	public function get_default_settings(): array {
		return [
			'enabled'      => false,
			'field_label'  => __( 'VAT number', 'kramar' ),
			'required_b2b' => false,
			// Default OFF: a syntactically valid VAT number is only a format
			// pass (no VIES/existence check), so auto-granting a VAT-free order
			// is a tax-leakage risk. Store owners must opt in. See G8.
			'exempt_valid' => false,
		];
	}

	/**
	 * {@inheritDoc}
	 */
	public function sanitize_settings( array $input ): array {
		return [
			'enabled'      => ! empty( $input['enabled'] ),
			'field_label'  => sanitize_text_field( $input['field_label'] ?? __( 'VAT number', 'kramar' ) ),
			'required_b2b' => ! empty( $input['required_b2b'] ),
			'exempt_valid' => ! empty( $input['exempt_valid'] ),
		];
	}

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

	/**
	 * {@inheritDoc}
	 */
	protected function run(): void {
		// Classic checkout field.
		add_filter( 'woocommerce_billing_fields', [ $this, 'add_billing_field' ] );

		// Blocks checkout field.
		// woocommerce_blocks_loaded has already fired by the time run() is called
		// (WC boots during plugins_loaded; Kramar's run() is called on init priority 0).
		// Call the registration method directly; it guards on function_exists() internally.
		$this->register_blocks_field();

		// Inline JS for real-time AJAX validation.
		add_action( 'wp_enqueue_scripts', [ $this, 'enqueue_validation_script' ] );

		// AJAX handlers.
		add_action( 'wp_ajax_kramar_validate_vat',        [ $this, 'ajax_validate_vat' ] );
		add_action( 'wp_ajax_nopriv_kramar_validate_vat', [ $this, 'ajax_validate_vat' ] );

		// Classic checkout validation.
		add_action( 'woocommerce_after_checkout_validation', [ $this, 'validate_checkout' ], 10, 2 );

		// Blocks checkout validation.
		add_action( 'woocommerce_checkout_validate_order_before_payment', [ $this, 'validate_blocks_checkout' ], 10, 2 );

		// Keep the session VAT-exempt state in sync with the live VAT field on every
		// classic checkout recalc, so reverse charge is applied reliably rather than
		// only after the one-shot AJAX validation.
		add_action( 'woocommerce_checkout_update_order_review', [ $this, 'sync_exemption_from_review' ] );

		// Store evidence on order (Classic).
		add_action( 'woocommerce_checkout_order_processed', [ $this, 'store_order_meta' ], 10, 3 );

		// Store evidence on order (Blocks / Store API).
		add_action( 'woocommerce_store_api_checkout_order_processed', [ $this, 'store_order_meta_blocks' ] );

		// Admin order billing fields.
		add_filter( 'woocommerce_admin_billing_fields', [ $this, 'add_admin_billing_field' ] );

		// Admin order view — show validation result below billing address.
		add_action( 'woocommerce_admin_order_data_after_billing_address', [ $this, 'render_admin_validation_result' ] );

		// My Account address page.
		add_filter( 'woocommerce_billing_fields', [ $this, 'add_my_account_billing_field' ] );

		// Orders-list column (HPOS + legacy).
		add_filter( 'manage_woocommerce_page_wc-orders_columns', [ $this, 'add_orders_column' ] );
		add_action( 'manage_woocommerce_page_wc-orders_custom_column', [ $this, 'render_orders_column' ], 10, 2 );
		add_filter( 'manage_edit-shop_order_columns', [ $this, 'add_orders_column' ] );
		add_action( 'manage_shop_order_posts_custom_column', [ $this, 'render_orders_column_legacy' ], 10, 2 );
	}

	/**
	 * Add a "VAT ID" column to the orders list (after the order total).
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, string> $columns Existing columns.
	 * @return array<string, string>
	 */
	public function add_orders_column( array $columns ): array {
		$new = [];
		foreach ( $columns as $key => $label ) {
			$new[ $key ] = $label;
			if ( 'order_total' === $key ) {
				$new['kramar_vat_id'] = __( 'VAT ID', 'kramar' );
			}
		}
		// If order_total wasn't found, append at the end.
		if ( ! isset( $new['kramar_vat_id'] ) ) {
			$new['kramar_vat_id'] = __( 'VAT ID', 'kramar' );
		}
		return $new;
	}

	/**
	 * Render the VAT ID column (HPOS).
	 *
	 * @since 1.0.0
	 *
	 * @param string    $column Column key.
	 * @param \WC_Order $order  Order.
	 */
	public function render_orders_column( string $column, \WC_Order $order ): void {
		if ( 'kramar_vat_id' === $column ) {
			echo wp_kses_post( $this->render_vat_badge( $order ) );
		}
	}

	/**
	 * Render the VAT ID column (legacy CPT).
	 *
	 * @since 1.0.0
	 *
	 * @param string $column  Column key.
	 * @param int    $post_id Order post ID.
	 */
	public function render_orders_column_legacy( string $column, int $post_id ): void {
		if ( 'kramar_vat_id' !== $column ) {
			return;
		}
		$order = wc_get_order( $post_id );
		if ( $order ) {
			echo wp_kses_post( $this->render_vat_badge( $order ) );
		}
	}

	/**
	 * Build the VAT ID cell: the number plus a small validity badge.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order Order.
	 * @return string HTML.
	 */
	private function render_vat_badge( \WC_Order $order ): string {
		$vat = (string) $order->get_meta( '_billing_vat_id' );
		if ( '' === $vat ) {
			return '<span style="color:#a7aaad;">&mdash;</span>';
		}

		$result = (string) $order->get_meta( '_vat_validation_result' );
		$colour = match ( $result ) {
			'valid'   => '#3a7d44',
			'invalid' => '#c00',
			default   => '#787c82',
		};

		return '<span style="font-family:monospace;">' . esc_html( $vat ) . '</span>'
			. ' <span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:' . esc_attr( $colour ) . ';" title="' . esc_attr( ucfirst( $result ?: 'not checked' ) ) . '"></span>';
	}

	// -------------------------------------------------------------------------
	// Classic checkout field
	// -------------------------------------------------------------------------

	/**
	 * Add the VAT ID field to WooCommerce Classic billing fields.
	 *
	 * Runs on `woocommerce_billing_fields`. Skips My Account pages so the
	 * field is only added once (see `add_my_account_billing_field` for that).
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, mixed> $fields Existing billing fields.
	 * @return array<string, mixed> Modified billing fields.
	 */
	public function add_billing_field( array $fields ): array {
		// My Account uses its own callback to avoid double-registering.
		if ( function_exists( 'is_account_page' ) && is_account_page() ) {
			return $fields;
		}

		$fields['billing_vat_id'] = [
			'type'        => 'text',
			'label'       => $this->get_label(),
			'placeholder' => 'NL123456789B01',
			'required'    => $this->is_required(),
			'class'       => [ 'form-row-wide' ],
			'priority'    => 35,
		];

		return $fields;
	}

	/**
	 * Add the VAT ID field to the My Account billing address form.
	 *
	 * Runs on `woocommerce_billing_fields` when viewing account pages.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, mixed> $fields Existing billing fields.
	 * @return array<string, mixed> Modified billing fields.
	 */
	public function add_my_account_billing_field( array $fields ): array {
		if ( ! function_exists( 'is_account_page' ) || ! is_account_page() ) {
			return $fields;
		}

		$fields['billing_vat_id'] = [
			'type'        => 'text',
			'label'       => $this->get_label(),
			'placeholder' => 'NL123456789B01',
			'required'    => false, // Never required on My Account — only at checkout.
			'class'       => [ 'form-row-wide' ],
			'priority'    => 35,
		];

		return $fields;
	}

	// -------------------------------------------------------------------------
	// Blocks checkout field
	// -------------------------------------------------------------------------

	/**
	 * Register the VAT ID field for the WooCommerce Blocks checkout.
	 *
	 * Called directly from run() because woocommerce_blocks_loaded has already
	 * fired by the time Kramar modules are initialised (init priority 0).
	 *
	 * @since 1.0.0
	 */
	public function register_blocks_field(): void {
		if ( ! function_exists( 'woocommerce_register_additional_checkout_field' ) ) {
			return;
		}

		woocommerce_register_additional_checkout_field( [
			'id'       => 'kramar/billing-vat-id',
			'label'    => $this->get_label(),
			'location' => 'address',
			'type'     => 'text',
			'required' => $this->is_required(),
		] );
	}

	// -------------------------------------------------------------------------
	// Inline JS — real-time field validation
	// -------------------------------------------------------------------------

	/**
	 * Enqueue the inline validation script on the checkout page.
	 *
	 * Attaches a small vanilla-JS blur listener to the VAT field. Uses
	 * `fetch()` (not jQuery) to call the AJAX handler.
	 *
	 * @since 1.0.0
	 */
	public function enqueue_validation_script(): void {
		if ( ! function_exists( 'is_checkout' ) || ! is_checkout() ) {
			return;
		}

		$ajax_url = admin_url( 'admin-ajax.php' );
		$nonce    = wp_create_nonce( 'kramar_validate_vat' );

		$label_checking  = esc_js( __( 'Checking…', 'kramar' ) );
		$label_valid     = esc_js( __( 'Valid VAT number', 'kramar' ) );
		$label_invalid   = esc_js( __( 'Invalid VAT number', 'kramar' ) );
		$label_error     = esc_js( __( 'Could not verify VAT number', 'kramar' ) );

		$script  = '(function () {' . "\n";
		$script .= "    'use strict';\n\n";
		$script .= '    var AJAX_URL = ' . $this->js_string( $ajax_url ) . ";\n";
		$script .= '    var NONCE    = ' . $this->js_string( $nonce ) . ";\n\n";
		$script .= "    function getField() {\n";
		$script .= "        return document.getElementById('billing_vat_id');\n";
		$script .= "    }\n\n";
		$script .= "    function getOrCreateStatus(field) {\n";
		$script .= "        var id  = 'kramar-vat-status';\n";
		$script .= "        var el  = document.getElementById(id);\n";
		$script .= "        if (!el) {\n";
		$script .= "            el = document.createElement('span');\n";
		$script .= "            el.id        = id;\n";
		$script .= "            el.className = 'kramar-vat-status';\n";
		$script .= "            el.style.cssText = 'display:inline-block;margin-left:6px;font-size:.85em;';\n";
		$script .= "            field.parentNode.appendChild(el);\n";
		$script .= "        }\n";
		$script .= "        return el;\n";
		$script .= "    }\n\n";
		$script .= "    function setStatus(status, field) {\n";
		$script .= "        var el = getOrCreateStatus(field);\n";
		$script .= "        if (status === 'checking') {\n";
		$script .= '            el.textContent = ' . $this->js_string( $label_checking ) . ";\n";
		$script .= "            el.style.color = '#888';\n";
		$script .= "        } else if (status === 'valid') {\n";
		$script .= '            el.textContent = ' . $this->js_string( '✓ ' . $label_valid ) . ";\n";
		$script .= "            el.style.color = '#3a7d44';\n";
		$script .= "        } else if (status === 'invalid') {\n";
		$script .= '            el.textContent = ' . $this->js_string( '✗ ' . $label_invalid ) . ";\n";
		$script .= "            el.style.color = '#c00';\n";
		$script .= "        } else {\n";
		$script .= '            el.textContent = ' . $this->js_string( '⚠ ' . $label_error ) . ";\n";
		$script .= "            el.style.color = '#b07d00';\n";
		$script .= "        }\n";
		$script .= "    }\n\n";
		$script .= "    function onBlur() {\n";
		$script .= "        var field = getField();\n";
		$script .= "        if (!field) { return; }\n";
		$script .= "        var val = field.value.trim();\n";
		$script .= "        if (!val) { return; }\n\n";
		$script .= "        setStatus('checking', field);\n\n";
		$script .= "        var body = new URLSearchParams();\n";
		$script .= "        body.append('action', 'kramar_validate_vat');\n";
		$script .= "        body.append('nonce', NONCE);\n";
		$script .= "        body.append('vat_id', val);\n\n";
		$script .= "        fetch(AJAX_URL, {\n";
		$script .= "            method      : 'POST',\n";
		$script .= "            credentials : 'same-origin',\n";
		$script .= "            headers     : { 'Content-Type': 'application/x-www-form-urlencoded' },\n";
		$script .= "            body        : body.toString()\n";
		$script .= "        })\n";
		$script .= "        .then(function (r) { return r.json(); })\n";
		$script .= "        .then(function (data) {\n";
		$script .= "            if (data && data.success) {\n";
		$script .= "                setStatus(data.data.valid ? 'valid' : 'invalid', field);\n";
		$script .= "                // The AJAX call above only flips the session VAT-exempt\n";
		$script .= "                // flag; nothing recalculates the order review. The VAT\n";
		$script .= "                // field itself carries no update_totals_on_change class, so\n";
		$script .= "                // without this the reverse charge silently never reached the\n";
		$script .= "                // total unless the customer happened to touch the country\n";
		$script .= "                // field again afterwards — which a returning B2B customer\n";
		$script .= "                // with a saved address never does.\n";
		$script .= "                if (window.jQuery) { window.jQuery(document.body).trigger('update_checkout'); }\n";
		$script .= "            } else {\n";
		$script .= "                setStatus('error', field);\n";
		$script .= "            }\n";
		$script .= "        })\n";
		$script .= "        .catch(function () { setStatus('error', field); });\n";
		$script .= "    }\n\n";
		$script .= "    function attachListener() {\n";
		$script .= "        var field = getField();\n";
		$script .= "        if (field) {\n";
		$script .= "            field.addEventListener('blur', onBlur);\n";
		$script .= "            return;\n";
		$script .= "        }\n";
		$script .= "        // Classic checkout renders the field after DOMContentLoaded via AJAX\n";
		$script .= "        // order-review updates; observe the form container for new nodes.\n";
		$script .= "        var form = document.querySelector('form.woocommerce-checkout');\n";
		$script .= "        if (!form) { return; }\n";
		$script .= "        var observer = new MutationObserver(function () {\n";
		$script .= "            var f = getField();\n";
		$script .= "            if (f) {\n";
		$script .= "                observer.disconnect();\n";
		$script .= "                f.addEventListener('blur', onBlur);\n";
		$script .= "            }\n";
		$script .= "        });\n";
		$script .= "        observer.observe(form, { childList: true, subtree: true });\n";
		$script .= "    }\n\n";
		$script .= "    if (document.readyState === 'loading') {\n";
		$script .= "        document.addEventListener('DOMContentLoaded', attachListener);\n";
		$script .= "    } else {\n";
		$script .= "        attachListener();\n";
		$script .= "    }\n";
		$script .= '}());';

		// $script is JavaScript. Every dynamic value it contains (ajax URL, nonce,
		// i18n labels) is embedded via js_string() -> wp_json_encode(), the correct
		// escaping for inline JS; esc_html()/esc_attr() would be wrong here.
		// Attach to Classic checkout handle (always registered by WC on checkout).
		if ( wp_script_is( 'wc-checkout', 'enqueued' ) || wp_script_is( 'wc-checkout', 'registered' ) ) {
			wp_add_inline_script( 'wc-checkout', $script );
			return;
		}

		// Blocks checkout.
		if ( wp_script_is( 'wc-blocks-checkout', 'enqueued' ) || wp_script_is( 'wc-blocks-checkout', 'registered' ) ) {
			wp_add_inline_script( 'wc-blocks-checkout', $script );
			return;
		}

		// Fallback: register a tiny dependency-free handle and enqueue it.
		wp_register_script( 'kramar-vat-validation', false, [], null, true ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion
		wp_enqueue_script( 'kramar-vat-validation' );
		wp_add_inline_script( 'kramar-vat-validation', $script );
	}

	// -------------------------------------------------------------------------
	// AJAX handler
	// -------------------------------------------------------------------------

	/**
	 * Handle the `kramar_validate_vat` AJAX request.
	 *
	 * Normalises the VAT ID and validates its format. When the format is valid
	 * and the `exempt_valid` setting is on, it marks the WooCommerce session
	 * customer as VAT-exempt so order totals re-calculate before the page
	 * refreshes.
	 *
	 * @since 1.0.0
	 */
	public function ajax_validate_vat(): void {
		check_ajax_referer( 'kramar_validate_vat', 'nonce' );

		$raw_vat = sanitize_text_field( wp_unslash( $_POST['vat_id'] ?? '' ) );

		if ( '' === $raw_vat ) {
			wp_send_json_error( [ 'message' => __( 'No VAT number provided.', 'kramar' ) ] );
		}

		// Normalise: remove spaces, dots, dashes.
		$normalised = strtoupper( (string) preg_replace( '/[\s.\-]/', '', $raw_vat ) );

		// Extract country prefix (first two letters if they are alpha).
		$country_code = '';
		$vat_number   = $normalised;

		if ( preg_match( '/^([A-Z]{2})(.+)$/', $normalised, $matches ) ) {
			$country_code = $matches[1];
			$vat_number   = $matches[2];
		}

		// Fall back to billing country from session when no prefix is present.
		if ( '' === $country_code && WC()->customer instanceof \WC_Customer ) {
			$country_code = strtoupper( WC()->customer->get_billing_country() );
		}

		// Format validation.
		$valid = $this->is_valid_format( $country_code, $vat_number );

		// Apply (or clear) session-level VAT exemption so WC recalculates totals.
		// Reverse charge only applies to cross-border intra-EU B2B — domestic B2B
		// stays taxed — see should_apply_reverse_charge().
		$reverse_charge = $this->should_apply_reverse_charge( $country_code, $vat_number );
		if ( WC()->customer instanceof \WC_Customer ) {
			WC()->customer->set_is_vat_exempt( $reverse_charge );
		}

		wp_send_json_success( [
			'valid'               => $valid,
			'reverse_charge'      => $reverse_charge,
			'name'                => '',
			'address'             => '',
			'consultation_number' => '',
		] );
	}

	/**
	 * Whether a reverse-charge VAT exemption should apply for a VAT number.
	 *
	 * Reverse charge (Art. 196 VAT Directive) applies only to cross-border
	 * intra-EU B2B: the buyer's VAT country must be an EU VAT country AND differ
	 * from the shop's base country. Domestic B2B is still taxed. Also requires the
	 * `exempt_valid` setting and a valid VAT number format.
	 *
	 * @since 1.0.0
	 *
	 * @param string $country_code Two-letter VAT country/prefix (e.g. NL, DE, EL).
	 * @param string $vat_number   VAT number without the country prefix.
	 * @return bool
	 */
	private function should_apply_reverse_charge( string $country_code, string $vat_number, string $customer_country = '' ): bool {
		if ( ! $this->is_exempt_valid() ) {
			return false;
		}
		if ( '' === $country_code || ! isset( self::VAT_PATTERNS[ $country_code ] ) ) {
			return false;
		}
		if ( ! $this->is_valid_format( $country_code, $vat_number ) ) {
			return false;
		}

		// Normalise Greece's VAT prefix (EL) to its ISO country code (GR).
		$vat_country = 'EL' === $country_code ? 'GR' : $country_code;
		$base        = strtoupper( (string) ( wc_get_base_location()['country'] ?? '' ) );
		$base        = 'EL' === $base ? 'GR' : $base;

		// No base country configured → cannot prove this is a cross-border
		// intra-EU supply, so do not exempt (would wrongly zero-rate domestic
		// B2B too). See G8.
		if ( '' === $base ) {
			return false;
		}

		if ( $vat_country === $base ) {
			return false;
		}

		// The VAT number's country must be where the customer actually is.
		// Only the typed prefix was checked before, against the shop's country
		// and nothing else — so a Dutch customer of a Dutch shop could type any
		// format-valid German number and have 21% removed from their own order.
		// Numbers are never verified against VIES (deliberately — no external
		// calls), which makes the address the only evidence there is that this
		// is a cross-border intra-EU supply. Without a country to compare
		// against, do not exempt.
		$customer_country = strtoupper( $customer_country );
		if ( '' === $customer_country ) {
			$customer_country = $this->customer_country();
		}
		$customer_country = 'EL' === $customer_country ? 'GR' : $customer_country;

		if ( '' === $customer_country ) {
			return false;
		}

		return $vat_country === $customer_country;
	}

	/**
	 * The country the customer says they are in, for the reverse-charge check.
	 *
	 * Shipping wins when set: for goods, where they go is what decides whether
	 * the supply is intra-EU. Falls back to billing.
	 *
	 * @since 1.0.2
	 */
	private function customer_country(): string {
		if ( ! WC()->customer instanceof \WC_Customer ) {
			return '';
		}

		$shipping = strtoupper( (string) WC()->customer->get_shipping_country() );
		if ( '' !== $shipping ) {
			return $shipping;
		}

		return strtoupper( (string) WC()->customer->get_billing_country() );
	}

	/**
	 * Set (or clear) the session VAT-exempt flag from a raw VAT ID string.
	 *
	 * @since 1.0.0
	 *
	 * @param string $raw_vat          Raw VAT ID as entered.
	 * @param string $fallback_country Billing country used when the number has no prefix.
	 */
	private function sync_session_exemption( string $raw_vat, string $fallback_country = '', string $customer_country = '' ): void {
		if ( ! WC()->customer instanceof \WC_Customer ) {
			return;
		}

		$normalised   = strtoupper( (string) preg_replace( '/[\s.\-]/', '', $raw_vat ) );
		$country_code = strtoupper( $fallback_country );
		$vat_number   = $normalised;
		if ( preg_match( '/^([A-Z]{2})(.+)$/', $normalised, $m ) ) {
			$country_code = $m[1];
			$vat_number   = $m[2];
		}

		// The country is passed in, not read back from the session. WooCommerce
		// fires woocommerce_checkout_update_order_review BEFORE it copies the
		// posted address onto WC()->customer, so anything reading the session
		// there sees the PREVIOUS request's country. That made an exemption
		// sticky: validate a DE number as a DE customer, then change the country
		// to NL, and the mismatch check compared the DE number against a stale DE
		// session and kept the order tax-free — the same domestic zero-rating the
		// country check exists to prevent, reached by a different route.
		WC()->customer->set_is_vat_exempt(
			$this->should_apply_reverse_charge( $country_code, $vat_number, $customer_country )
		);
	}

	/**
	 * Keep the session exemption in sync whenever the classic checkout order-review
	 * recalculates, so the displayed totals — and the order that gets submitted —
	 * reflect reverse charge reliably (not only after the one-shot AJAX validation).
	 *
	 * @since 1.0.0
	 *
	 * @param string $post_data URL-encoded checkout form data.
	 */
	public function sync_exemption_from_review( string $post_data ): void {
		$data = [];
		parse_str( $post_data, $data );
		$vat     = sanitize_text_field( (string) ( $data['billing_vat_id'] ?? '' ) );
		$country = sanitize_text_field( (string) ( $data['billing_country'] ?? '' ) );

		// Where the goods are going decides whether the supply is intra-EU, so
		// shipping wins when the shopper is shipping elsewhere. Taken from this
		// request's own POST — the session is not updated until after this runs.
		$ship_to  = ! empty( $data['ship_to_different_address'] );
		$shipping = sanitize_text_field( (string) ( $data['shipping_country'] ?? '' ) );
		$customer_country = ( $ship_to && '' !== $shipping ) ? $shipping : $country;

		$this->sync_session_exemption( $vat, $country, $customer_country );
	}

	// -------------------------------------------------------------------------
	// Checkout validation
	// -------------------------------------------------------------------------

	/**
	 * Validate the VAT field during Classic checkout submission.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, mixed> $data   Posted checkout data.
	 * @param \WP_Error            $errors Error collector.
	 */
	public function validate_checkout( array $data, \WP_Error $errors ): void {
		$vat_id = sanitize_text_field( $data['billing_vat_id'] ?? '' );

		if ( $this->is_required() && '' === $vat_id ) {
			$errors->add(
				'kramar_vat_id_required',
				sprintf(
					/* translators: %s: field label */
					__( '%s is a required field.', 'kramar' ),
					'<strong>' . esc_html( $this->get_label() ) . '</strong>'
				)
			);
		}
	}

	/**
	 * Validate the VAT field during Blocks checkout (Store API).
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order  The order object populated with address data.
	 * @param \WP_Error $errors Validation error collector.
	 */
	public function validate_blocks_checkout( \WC_Order $order, \WP_Error $errors ): void {
		// Blocks checkout stores the additional field value on the order meta.
		$vat_id = sanitize_text_field(
			(string) $order->get_meta( 'kramar/billing-vat-id', true )
		);

		if ( $this->is_required() && '' === $vat_id ) {
			$errors->add(
				'kramar_vat_id_required',
				sprintf(
					/* translators: %s: field label */
					__( '%s is a required field.', 'kramar' ),
					'<strong>' . esc_html( $this->get_label() ) . '</strong>'
				)
			);
		}
	}

	// -------------------------------------------------------------------------
	// Order meta — evidence storage
	// -------------------------------------------------------------------------

	/**
	 * Store VAT validation evidence on the order (Classic checkout).
	 *
	 * @since 1.0.0
	 *
	 * @param int                  $order_id    Newly created order ID.
	 * @param array<string, mixed> $posted_data Posted checkout data.
	 * @param \WC_Order            $order       Order object.
	 */
	public function store_order_meta( int $order_id, array $posted_data, \WC_Order $order ): void {
		$vat_id = sanitize_text_field( $posted_data['billing_vat_id'] ?? '' );
		$this->write_vat_meta( $order, $vat_id );
	}

	/**
	 * Store VAT validation evidence on the order (Blocks / Store API checkout).
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order Order object.
	 */
	public function store_order_meta_blocks( \WC_Order $order ): void {
		$vat_id = sanitize_text_field(
			(string) $order->get_meta( 'kramar/billing-vat-id', true )
		);
		$this->write_vat_meta( $order, $vat_id );
	}

	/**
	 * Write VAT meta fields to an order and optionally apply tax exemption.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order  Order object.
	 * @param string    $vat_id Raw VAT ID string supplied at checkout.
	 */
	private function write_vat_meta( \WC_Order $order, string $vat_id ): void {
		$vat_id = strtoupper( (string) preg_replace( '/[\s.\-]/', '', $vat_id ) );

		$validation_result      = 'not_checked';
		$consultation_number    = '';
		$company_name           = '';
		$reverse_charge_applied = false;

		if ( '' !== $vat_id ) {
			// Re-parse country + number.
			$country_code = '';
			$vat_number   = $vat_id;

			if ( preg_match( '/^([A-Z]{2})(.+)$/', $vat_id, $matches ) ) {
				$country_code = $matches[1];
				$vat_number   = $matches[2];
			}

			if ( '' !== $country_code && $this->is_valid_format( $country_code, $vat_number ) ) {
				$validation_result = 'valid';

				// Record reverse charge as applied only when the policy applies AND
				// the order actually came out tax-free — so the note/evidence never
				// claims an exemption that didn't take effect during calculation.
				$order_country = strtoupper( (string) $order->get_shipping_country() );
				if ( '' === $order_country ) {
					$order_country = strtoupper( (string) $order->get_billing_country() );
				}

				if ( $this->should_apply_reverse_charge( $country_code, $vat_number, $order_country )
					&& (float) $order->get_total_tax() <= 0.0
				) {
					$reverse_charge_applied = true;
					$order->set_customer_note(
						trim(
							$order->get_customer_note() . "\n" .
							__( 'VAT reverse charge applied — intra-EU B2B, VAT number format validated.', 'kramar' )
						)
					);
				}
			} else {
				$validation_result = 'invalid';
			}
		}

		$order->update_meta_data( '_billing_vat_id', $vat_id );
		$order->update_meta_data( '_vat_validation_result', $validation_result );
		$order->update_meta_data( '_vat_reverse_charge_applied', $reverse_charge_applied ? 'yes' : 'no' );
		$order->update_meta_data( '_vat_consultation_number', $consultation_number );
		$order->update_meta_data( '_vat_company_name', $company_name );
		$order->update_meta_data( '_vat_ip_country', $this->get_ip_country() );
		$order->save();
	}

	// -------------------------------------------------------------------------
	// Admin order view
	// -------------------------------------------------------------------------

	/**
	 * Add the VAT ID to WooCommerce admin billing fields.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string, mixed> $fields Existing admin billing fields.
	 * @return array<string, mixed> Modified admin billing fields.
	 */
	public function add_admin_billing_field( array $fields ): array {
		$fields['vat_id'] = [
			'label' => $this->get_label(),
			'show'  => false,
		];

		return $fields;
	}

	/**
	 * Render the VAT validation result panel below the billing address block.
	 *
	 * @since 1.0.0
	 *
	 * @param \WC_Order $order Current admin order object.
	 */
	public function render_admin_validation_result( \WC_Order $order ): void {
		$vat_id      = (string) $order->get_meta( '_billing_vat_id', true );
		$result      = (string) $order->get_meta( '_vat_validation_result', true );
		$consult_no  = (string) $order->get_meta( '_vat_consultation_number', true );
		$company     = (string) $order->get_meta( '_vat_company_name', true );
		$ip_country  = (string) $order->get_meta( '_vat_ip_country', true );

		if ( '' === $vat_id && '' === $result ) {
			return;
		}

		$badge_colour = match ( $result ) {
			'valid'       => '#3a7d44',
			'invalid'     => '#c00',
			'not_checked' => '#888',
			default       => '#888',
		};

		$result_label = match ( $result ) {
			'valid'       => __( 'Valid', 'kramar' ),
			'invalid'     => __( 'Invalid', 'kramar' ),
			'not_checked' => __( 'Not checked', 'kramar' ),
			default       => __( 'Unknown', 'kramar' ),
		};

		?>
		<div style="margin-top:12px;padding:10px 12px;border:1px solid #ddd;border-radius:4px;background:#fafafa;">
			<strong><?php esc_html_e( 'VAT Validation', 'kramar' ); ?></strong><br>
			<?php if ( '' !== $vat_id ) : ?>
				<span style="font-family:monospace;"><?php echo esc_html( $vat_id ); ?></span>
				<span style="margin-left:8px;padding:2px 7px;border-radius:3px;font-size:.8em;color:#fff;background:<?php echo esc_attr( $badge_colour ); ?>;">
					<?php echo esc_html( $result_label ); ?>
				</span>
				<br>
			<?php endif; ?>
			<?php if ( '' !== $company ) : ?>
				<span style="color:#555;"><?php esc_html_e( 'Company:', 'kramar' ); ?> <?php echo esc_html( $company ); ?></span><br>
			<?php endif; ?>
			<?php if ( '' !== $consult_no ) : ?>
				<span style="color:#555;font-size:.85em;"><?php esc_html_e( 'Consultation #:', 'kramar' ); ?> <?php echo esc_html( $consult_no ); ?></span><br>
			<?php endif; ?>
			<?php if ( '' !== $ip_country ) : ?>
				<span style="color:#555;font-size:.85em;"><?php esc_html_e( 'IP country:', 'kramar' ); ?> <?php echo esc_html( $ip_country ); ?></span>
			<?php endif; ?>
		</div>
		<?php
	}

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

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

		$field_label  = (string) $this->plugin->get_setting( $cat, $slug, 'field_label' );
		$required_b2b = (bool)   $this->plugin->get_setting( $cat, $slug, 'required_b2b' );
		$exempt_valid = (bool)   $this->plugin->get_setting( $cat, $slug, 'exempt_valid' );

		?>
		<div class="kramar-module__extra">
			<p>
				<label for="kramar-vat-id-label"><?php esc_html_e( 'Field label', 'kramar' ); ?></label>
				<input
					type="text"
					id="kramar-vat-id-label"
					name="<?php echo esc_attr( $option_name ); ?>[<?php echo esc_attr( $cat ); ?>][<?php echo esc_attr( $slug ); ?>][field_label]"
					value="<?php echo esc_attr( $field_label ); ?>"
					class="regular-text"
				/>
				<span class="description"><?php esc_html_e( 'Label shown next to the VAT field on the checkout and My Account pages.', 'kramar' ); ?></span>
			</p>
			<p>
				<label>
					<input
						type="checkbox"
						name="<?php echo esc_attr( $option_name ); ?>[<?php echo esc_attr( $cat ); ?>][<?php echo esc_attr( $slug ); ?>][required_b2b]"
						value="1"
						<?php checked( $required_b2b ); ?>
					/>
					<?php esc_html_e( 'Required — make the VAT number mandatory (B2B-only shops)', 'kramar' ); ?>
				</label>
			</p>
			<p>
				<label>
					<input
						type="checkbox"
						name="<?php echo esc_attr( $option_name ); ?>[<?php echo esc_attr( $cat ); ?>][<?php echo esc_attr( $slug ); ?>][exempt_valid]"
						value="1"
						<?php checked( $exempt_valid ); ?>
					/>
					<?php esc_html_e( 'Auto-exempt valid VAT IDs from tax (reverse charge)', 'kramar' ); ?>
				</label>
			</p>
		</div>
		<?php
	}

	// -------------------------------------------------------------------------
	// Format helpers
	// -------------------------------------------------------------------------

	/**
	 * Check whether a country+number pair passes local format validation.
	 *
	 * Verifies that the country code is a recognised EU member and that the
	 * number part meets the minimum character count for that country.
	 *
	 * @since 1.0.0
	 *
	 * @param string $country_code Two-letter code (e.g. 'NL').
	 * @param string $vat_number   Number part (no country prefix).
	 * @return bool True when format looks plausible.
	 */
	private function is_valid_format( string $country_code, string $vat_number ): bool {
		if ( ! isset( self::VAT_PATTERNS[ $country_code ] ) ) {
			return false;
		}

		if ( strlen( $vat_number ) < self::VAT_PATTERNS[ $country_code ] ) {
			return false;
		}

		if ( isset( self::VAT_REGEX[ $country_code ] ) ) {
			return 1 === preg_match( self::VAT_REGEX[ $country_code ], strtoupper( $vat_number ) );
		}

		return true;
	}

	/**
	 * Attempt to resolve the visitor's country from their IP address.
	 *
	 * Used as a second piece of evidence for EU digital-services rules.
	 * Returns an empty string when the IP cannot be resolved.
	 *
	 * @since 1.0.0
	 *
	 * @return string ISO-3166-1 alpha-2 country code, or empty string.
	 */
	private function get_ip_country(): string {
		// WooCommerce Geolocation is available on most WC installs.
		if ( class_exists( '\WC_Geolocation' ) ) {
			$geo     = new \WC_Geolocation();
			$country = $geo->geolocate_ip( \WC_Geolocation::get_ip_address() );
			return $country['country'] ?? '';
		}

		return '';
	}

	// -------------------------------------------------------------------------
	// Convenience getters
	// -------------------------------------------------------------------------

	/**
	 * Return the configured field label.
	 *
	 * @since 1.0.0
	 *
	 * @return string Field label.
	 */
	private function get_label(): string {
		$label = (string) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'field_label' );
		return '' !== $label ? $label : __( 'VAT number', 'kramar' );
	}

	/**
	 * Return whether the VAT field is required (B2B mode).
	 *
	 * @since 1.0.0
	 *
	 * @return bool True when the field is mandatory.
	 */
	private function is_required(): bool {
		return (bool) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'required_b2b' );
	}

	/**
	 * Return whether valid VAT IDs should automatically exempt tax.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True when auto-exemption is on.
	 */
	private function is_exempt_valid(): bool {
		return (bool) $this->plugin->get_setting( $this->get_category(), $this->get_slug(), 'exempt_valid' );
	}

	/**
	 * Return a JSON-encoded JavaScript string literal.
	 *
	 * Used to safely embed PHP values inside the inline JS block.
	 *
	 * @since 1.0.0
	 *
	 * @param string $value Raw PHP string.
	 * @return string JSON string including surrounding quotes.
	 */
	private function js_string( string $value ): string {
		return (string) wp_json_encode( $value );
	}
}
