<?php

namespace EasyShippingRate;

use Automattic\WooCommerce\Utilities\NumberUtil;
use EasyShippingRate\Admin\Admin;
use function EasyShippingRate\Support\get_cart_volume;
use function EasyShippingRate\Support\get_user_roles;
use function EasyShippingRate\Support\is_premium;

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

class ShippingMethod extends \WC_Shipping_Method {
	const METHOD_TITLE               = 'method_title';
	const METHOD_DESCRIPTION         = 'method_description';
	const METHOD_TAXABLE             = 'method_taxable';
	const METHOD_START_DATE          = 'method_start_date';
	const METHOD_END_DATE            = 'method_end_date';
	const METHOD_FREE_SHIPPING_COST  = 'method_free_shipping_cost';
	const METHOD_FREE_REQUIRES       = 'method_free_requires';
	const METHOD_FREE_MIN_AMOUNT     = 'method_free_min_amount';
	const METHOD_FREE_IGN_DISC       = 'method_free_ignore_discounts';
	const METHOD_FREE_USER_POSTCODE  = 'method_free_user_postcode';
	const METHOD_FREE_SHIPPING_LABEL = 'method_free_shipping_label';
	const METHOD_FREE_NOTIFICATION   = 'method_free_shipping_notification';
	const METHOD_VISIBILITY          = 'method_visibility';
	const METHOD_DEFAULT             = 'method_DEFAULT';
	const METHOD_MINIMUM_COST        = 'method_minimum_cost';
	const METHOD_MAXIMUM_COST        = 'method_maximum_cost';
	const METHOD_ESTIMATED_DELIVERY  = 'method_estimated_delivery';
	const METHOD_RULE_CALCULATION    = 'method_rule_calculation';
	const METHOD_DIM_FACTOR          = 'method_dim_factor';
	const METHOD_DEBUG               = 'method_debug';
	const METHOD_ID                  = 'esraw';
	const CONFIG_HIDE_ALL            = 'config_hide_method';
	const CONFIG_REMOVE_DATA         = 'config_remove_data_on_uninstall';

	/**
	 * Min amount to be valid.
	 *
	 * @var integer
	 */
	private $min_amount = 0;

	/**
	 * Requires option.
	 *
	 * @var string
	 */
	private $requires = '';

	/**
	 * Whether to ignore discounts when calculating the free-shipping threshold.
	 *
	 * @var string
	 */
	private $ignore_discounts = '';

	/**
	 * Requires postcodes.
	 *
	 * @var string
	 */
	private $postcode_need = '';

	/**
	 * Whether or not is free shipping.
	 *
	 * @var string
	 */
	public $is_free_shipping;

	const CONDITION_CHOICES  = array(
		'Cart'                => array(
			'Subtotal'                => 'subtotal',
			'Subtotal ex. taxes'      => 'subtotal_ex',
			'Quantity'                => 'quantity',
			'Cart line item'          => 'cart_line_item',
			'Contains shipping class' => 'contains_shipping_class',
		),
		'Weight & Dimensions' => array(
			'Weight'             => 'weight',
			'Volume'             => 'volume',
			'Dimensional weight' => 'dim_weight',
		),
		'User Details'        => array(
			'Zipcode'   => 'zipcode',
			'City'      => 'city',
			'State'     => 'state',
			'Country'   => 'country',
			'User role' => 'user_roles',
		),
		'Products'            => array(
			'Contains product' => 'contains_product',
		),
	);
	const OPERATOR           = array(
		'is'     => 'is',
		'is_not' => 'is not',
	);
	const CONDITIONS_ACTIONS = array(
		'none'      => 'Continue to next rule',
		'stop'      => 'Stop here — use this cost',
		'cancel'    => 'Hide this shipping method',
		'free_ship' => 'Apply free shipping — stop',
	);

	/**
	 * Ready-made rule sets offered in the "Load a template" dropdown. Values are
	 * deliberately ordinary (5.00, 50, tier bounds) — they are starting points the
	 * merchant is expected to adjust, not recommendations.
	 *
	 * @return array
	 */
	public static function rule_templates() {
		return array(
			'flat_rate'         => array(
				'label' => __( 'Flat rate', 'easy-shipping-rate' ),
				'rules' => array(
					array(
						'condition' => 'subtotal',
						'operator'  => 'is',
						'operand1'  => '0',
						'operand2'  => '',
						'cost'      => '5.00',
						'action'    => 'stop',
					),
				),
			),
			'free_above_amount' => array(
				'label' => __( 'Free shipping above an amount', 'easy-shipping-rate' ),
				'rules' => array(
					array(
						'condition' => 'subtotal',
						'operator'  => 'is',
						'operand1'  => '0',
						'operand2'  => '50',
						'cost'      => '5.00',
						'action'    => 'stop',
					),
					array(
						'condition' => 'subtotal',
						'operator'  => 'is',
						'operand1'  => '50',
						'operand2'  => '',
						'cost'      => '0',
						'action'    => 'free_ship',
					),
				),
			),
			'weight_tiers'      => array(
				'label' => __( 'Weight-based tiers', 'easy-shipping-rate' ),
				'rules' => array(
					array(
						'condition' => 'weight',
						'operator'  => 'is',
						'operand1'  => '0',
						'operand2'  => '5',
						'cost'      => '4.90',
						'action'    => 'stop',
					),
					array(
						'condition' => 'weight',
						'operator'  => 'is',
						'operand1'  => '5',
						'operand2'  => '20',
						'cost'      => '9.90',
						'action'    => 'stop',
					),
					array(
						'condition' => 'weight',
						'operator'  => 'is',
						'operand1'  => '20',
						'operand2'  => '',
						'cost'      => '19.90',
						'action'    => 'stop',
					),
				),
			),
			'free_postcodes'    => array(
				'label' => __( 'Free shipping for specific postcodes', 'easy-shipping-rate' ),
				'rules' => array(
					array(
						'condition' => 'zipcode',
						'operator'  => 'is',
						'choices'   => '75001, 75002',
						'cost'      => '0',
						'action'    => 'free_ship',
					),
					array(
						'condition' => 'subtotal',
						'operator'  => 'is',
						'operand1'  => '0',
						'operand2'  => '',
						'cost'      => '5.00',
						'action'    => 'stop',
					),
				),
			),
		);
	}

	/**
	 * @var mixed
	 */
	private $conditions_option_key;

	/**
	 * @var mixed
	 */
	private $conditions_key;

	/**
	 * @var mixed
	 */
	private $conditions_options;

	/**
	 * Constructor for your shipping class
	 *
	 * @access public
	 * @return void
	 */
	public function __construct( $instance_id = 0 ) {
		$this->instance_id = absint( $instance_id );
		$this->supports    = array(
			'shipping-zones',
			'instance-settings',
		);

		$this->id = self::METHOD_ID;
		$this->init();

		$this->method_title       = __( 'Easy Shipping', 'easy-shipping-rate' );
		$this->method_description = __( 'Easy way to define your shipping method', 'easy-shipping-rate' );
		$this->enabled            = isset( $this->settings['enabled'] ) ? $this->settings['enabled'] : 'yes';
		$this->title              = $this->get_option( self::METHOD_TITLE, $this->method_title );
		$this->min_amount         = $this->get_option( self::METHOD_FREE_MIN_AMOUNT, 0 );
		$this->postcode_need      = $this->get_option( self::METHOD_FREE_USER_POSTCODE, null );
		$this->requires           = $this->get_option( self::METHOD_FREE_REQUIRES );
		$this->ignore_discounts   = $this->get_option( self::METHOD_FREE_IGN_DISC );

		$this->tax_status = $this->get_option( self::METHOD_TAXABLE );

		$this->conditions_key        = 'easy_rate';
		$this->conditions_option_key = $this->id . $this->instance_id;
		$this->conditions_options    = get_option( $this->conditions_option_key, array() );

		add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_admin_js' ) );
	}

	/**
	 * Return only our own instance fields, bypassing the WC filter that
	 * third-party plugins use to inject sections into every shipping method.
	 *
	 * @return array
	 */
	public function get_instance_form_fields() {
		return array_map( array( $this, 'set_defaults' ), $this->instance_form_fields );
	}

	/**
	 * Persist the condition rows alongside the regular settings. Saving here —
	 * instead of in the constructor — means only the instance actually being
	 * edited writes to the database, rather than every instantiation (including
	 * front-end cart and checkout requests) re-saving on each page load.
	 *
	 * @return bool
	 */
	public function process_admin_options() {
		$saved = parent::process_admin_options();

		if ( isset( $_POST['esr_securite_nonce'] ) && wp_verify_nonce( sanitize_key( wp_unslash( $_POST['esr_securite_nonce'] ) ), 'esr-security' ) ) {
			$old_conditions    = (array) get_option( $this->conditions_option_key, array() );
			$posted_conditions = ( isset( $_POST[ $this->conditions_key ] ) && is_array( $_POST[ $this->conditions_key ] ) ) ? wp_unslash( $_POST[ $this->conditions_key ] ) : array();
			$clean_conditions  = self::sanitize_conditions( $posted_conditions );

			// Enforce the inline-delete donation gate on the server, so it cannot
			// be bypassed from the browser. Over the quota, keep the old rules.
			$deleted = count( $old_conditions ) - count( $clean_conditions );
			if ( $deleted > 0 && ! Admin::allow_rule_deletions( $deleted ) ) {
				$clean_conditions = $old_conditions;
			}

			update_option( $this->conditions_option_key, $clean_conditions );
			$this->conditions_options = $clean_conditions;
		}

		if ( $saved ) {
			$this->register_translated_strings();
		}

		return $saved;
	}

	/**
	 * Register the per-instance user-entered strings with WPML and Polylang so
	 * they appear in their respective translation interfaces.
	 *
	 * Called after save so the registered values are always up to date. The
	 * context key includes the instance id to keep instances independent.
	 */
	private function register_translated_strings(): void {
		if ( 0 === $this->instance_id ) {
			return;
		}
		$strings = array(
			'Method title'        => $this->get_instance_option( self::METHOD_TITLE, '' ),
			'Method description'  => $this->get_instance_option( self::METHOD_DESCRIPTION, '' ),
			'Free shipping label' => $this->get_instance_option( self::METHOD_FREE_SHIPPING_LABEL, '' ),
		);
		foreach ( $strings as $name => $value ) {
			if ( '' === $value ) {
				continue;
			}
			$key = sprintf( '%s (instance %d)', $name, $this->instance_id );
			do_action( 'wpml_register_single_string', 'easy-shipping-rate', $key, $value );
			if ( function_exists( 'pll_register_string' ) ) {
				pll_register_string( $key, $value, 'easy-shipping-rate' );
			}
		}
	}

	/**
	 * Return the WPML/Polylang translation of a stored instance option value.
	 *
	 * Falls back to the raw value when neither plugin is active or no translation
	 * exists for the current language.
	 *
	 * @param string $option_key  ShippingMethod::METHOD_* constant.
	 * @param string $string_name Human-readable name used as the registration key.
	 * @param string $default     Fallback when the option is empty.
	 * @return string
	 */
	public function get_translated_instance_option( string $option_key, string $string_name, string $default = '' ): string {
		$value = $this->get_instance_option( $option_key, $default );
		if ( '' === $value ) {
			return $default;
		}
		$key   = sprintf( '%s (instance %d)', $string_name, $this->instance_id );
		$value = (string) apply_filters( 'wpml_translate_single_string', $value, 'easy-shipping-rate', $key );
		if ( function_exists( 'pll__' ) ) {
			$translated = pll__( $value );
			if ( '' !== $translated ) {
				$value = $translated;
			}
		}
		return $value;
	}

	/**
	 * Sanitize posted condition rows before persisting them, so the settings
	 * form cannot write arbitrary data to the database.
	 *
	 * @param array $conditions Raw, unslashed condition rows from the request.
	 * @return array Sanitized condition rows.
	 */
	private static function sanitize_conditions( $conditions ) {
		$clean = array();
		foreach ( (array) $conditions as $row_key => $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}
			$clean_row = array();
			foreach ( $row as $field => $value ) {
				$field = sanitize_key( $field );
				if ( is_array( $value ) ) {
					$clean_row[ $field ] = array_map( 'sanitize_text_field', $value );
				} else {
					$clean_row[ $field ] = sanitize_text_field( $value );
				}
			}
			$clean[ absint( $row_key ) ] = $clean_row;
		}
		return $clean;
	}

	/**
	 * Resolve a rule cost: either a plain number ("12.50") or a formula using
	 * cart tokens ("5 + [qty]*1.50", "[subtotal]*0.08").
	 *
	 * @param string $cost    Raw cost saved on the rule.
	 * @param array  $package Shipping package, source of the subtotal token.
	 * @return float
	 */
	private function resolve_rule_cost( $cost, $package ) {
		$cost = (string) $cost;
		if ( false === strpos( $cost, '[' ) ) {
			return (float) $cost;
		}

		$tokens = array(
			'[qty]'         => WC()->cart->get_cart_contents_count(),
			'[items]'       => count( WC()->cart->get_cart() ),
			'[weight]'      => WC()->cart->get_cart_contents_weight(),
			'[subtotal]'    => isset( $package['cart_subtotal'] ) ? $package['cart_subtotal'] : 0,
			'[subtotal_ex]' => WC()->cart->get_subtotal(),
			'[volume]'      => get_cart_volume(),
		);

		$expression = $cost;
		foreach ( $tokens as $token => $value ) {
			// %F keeps the decimal point locale-independent.
			$expression = str_ireplace( $token, sprintf( '%.6F', (float) $value ), $expression );
		}

		return self::evaluate_formula( $expression );
	}

	/**
	 * Safely evaluate a basic arithmetic expression (+ - * / and parentheses).
	 * wp.org forbids eval(), so this is a small shunting-yard evaluator; any
	 * malformed input yields 0 instead of an error.
	 *
	 * @param string $expression Expression with tokens already substituted.
	 * @return float
	 */
	private static function evaluate_formula( $expression ) {
		if ( ! preg_match( '/^[0-9+\-*\/(). ]+$/', $expression ) ) {
			return 0.0;
		}
		preg_match_all( '/\d+\.?\d*|\.\d+|[+\-*\/()]/', $expression, $matches );
		$tokens = $matches[0];
		if ( empty( $tokens ) ) {
			return 0.0;
		}

		$precedence = array(
			'+' => 1,
			'-' => 1,
			'*' => 2,
			'/' => 2,
			'u' => 3, // Unary minus.
		);
		$output     = array();
		$stack      = array();
		$prev       = null;

		// Shunting-yard: infix to postfix.
		foreach ( $tokens as $token ) {
			if ( is_numeric( $token ) ) {
				$output[] = (float) $token;
			} elseif ( '(' === $token ) {
				$stack[] = $token;
			} elseif ( ')' === $token ) {
				while ( ! empty( $stack ) && '(' !== end( $stack ) ) {
					$output[] = array_pop( $stack );
				}
				if ( empty( $stack ) ) {
					return 0.0; // Unbalanced parentheses.
				}
				array_pop( $stack );
			} else {
				$after_operand = null !== $prev && '(' !== $prev && ! isset( $precedence[ $prev ] );
				if ( ! $after_operand ) {
					if ( '-' === $token ) {
						$stack[] = 'u';
						$prev    = '-';
						continue;
					}
					return 0.0; // Operator without a left operand ("*5", "5+*2").
				}
				while ( ! empty( $stack ) && isset( $precedence[ end( $stack ) ] ) && $precedence[ end( $stack ) ] >= $precedence[ $token ] ) {
					$output[] = array_pop( $stack );
				}
				$stack[] = $token;
			}
			$prev = $token;
		}
		while ( ! empty( $stack ) ) {
			$op = array_pop( $stack );
			if ( '(' === $op ) {
				return 0.0;
			}
			$output[] = $op;
		}

		// Evaluate the postfix sequence.
		$result = array();
		foreach ( $output as $token ) {
			if ( is_float( $token ) ) {
				$result[] = $token;
				continue;
			}
			if ( 'u' === $token ) {
				if ( empty( $result ) ) {
					return 0.0;
				}
				$result[] = -array_pop( $result );
				continue;
			}
			if ( count( $result ) < 2 ) {
				return 0.0;
			}
			$b = array_pop( $result );
			$a = array_pop( $result );
			if ( '+' === $token ) {
				$result[] = $a + $b;
			} elseif ( '-' === $token ) {
				$result[] = $a - $b;
			} elseif ( '*' === $token ) {
				$result[] = $a * $b;
			} else {
				$result[] = 0.0 === (float) $b ? 0.0 : $a / $b;
			}
		}

		return 1 === count( $result ) ? round( (float) $result[0], 4 ) : 0.0;
	}

	/**
	 * Init your settings
	 *
	 * @access public
	 * @return void
	 */
	public function init() {
		// Load the settings API.
		$this->init_form_fields(); // This is part of the settings API. Override the method to add your own settings.
		$this->init_settings(); // This is part of the settings API. Loads settings you previously init.

		// Save settings in admin if you have any defined.
		add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
	}

	/**
	 * Initialise Gateway Settings Form Fields
	 */
	public function init_form_fields() {

		$settings = array(
			'section_general'                => array(
				'title' => __( 'General Settings', 'easy-shipping-rate' ),
				'type'  => 'title',
				'class' => 'esraw-text-2xl esraw-font-bold',
			),
			self::METHOD_TITLE               => array(
				'title'       => __( 'Method Title', 'easy-shipping-rate' ),
				'type'        => 'text',
				'placeholder' => __( 'e.g. Standard Delivery, Express, Free Shipping…', 'easy-shipping-rate' ),
				'description' => __( 'This controls the title which the user sees during checkout.', 'easy-shipping-rate' ),
				'default'     => $this->method_title,
				'desc_tip'    => true,
			),
			self::METHOD_DESCRIPTION         => array(
				'title'       => __( 'Description', 'easy-shipping-rate' ),
				'type'        => 'text',
				'description' => __( 'This controls the description which the user sees during checkout.', 'easy-shipping-rate' ),
				'desc_tip'    => true,
			),
			self::METHOD_TAXABLE             => array(
				'title'       => __( 'Tax Status', 'easy-shipping-rate' ),
				'type'        => 'select',
				'default'     => 'taxable',
				'options'     => array(
					'taxable' => __( 'Taxable', 'easy-shipping-rate' ),
					'none'    => _x( 'None', 'Tax status', 'easy-shipping-rate' ),
				),
				'description' => __( 'Apply tax or no.', 'easy-shipping-rate' ),
				'desc_tip'    => true,
			),
			self::METHOD_START_DATE          => array(
				'title'       => __( 'Start Date', 'easy-shipping-rate' ),
				'type'        => 'date',
				'default'     => null,
				'description' => sprintf(
					/* translators: %s: store timezone name, e.g. Europe/Paris */
					__( 'This method is hidden before this date. Leave empty for no start limit. Uses the store timezone (%s).', 'easy-shipping-rate' ),
					wp_timezone_string()
				),
				'desc_tip'    => false,
			),
			self::METHOD_END_DATE            => array(
				'title'       => __( 'End Date', 'easy-shipping-rate' ),
				'type'        => 'date',
				'default'     => null,
				'description' => sprintf(
					/* translators: %s: store timezone name, e.g. Europe/Paris */
					__( 'This method is automatically hidden from checkout when this date passes. Leave empty for no end limit. Uses the store timezone (%s).', 'easy-shipping-rate' ),
					wp_timezone_string()
				),
				'desc_tip'    => false,
			),

			'section_free_shipping'          => array(
				'title'       => __( 'Free Shipping', 'easy-shipping-rate' ),
				'type'        => 'title',
				'class'       => 'esraw-text-2xl esraw-font-bold',
				'description' => __( 'Via the fields below (coupon, postcode, or minimum amount), or via a rule row with action "Apply free shipping — stop" for more complex conditions. Both work independently.', 'easy-shipping-rate' ),
			),
			self::METHOD_FREE_REQUIRES       => array(
				'title'       => __( 'Free shipping requires', 'easy-shipping-rate' ),
				'type'        => 'select',
				'class'       => 'wc-enhanced-select',
				'default'     => '',
				'options'     => array(
					''           => __( 'N/A', 'easy-shipping-rate' ),
					'postcode'   => __( 'A user postcode', 'easy-shipping-rate' ),
					'coupon'     => __( 'A valid free shipping coupon', 'easy-shipping-rate' ),
					'min_amount' => __( 'A minimum order amount', 'easy-shipping-rate' ),
					'either'     => __( 'A minimum order amount OR a coupon', 'easy-shipping-rate' ),
					'both'       => __( 'A minimum order amount AND a coupon', 'easy-shipping-rate' ),
				),
				'description' => __( 'Condition for free shipping. Choose N/A to disable it.', 'easy-shipping-rate' ),
				'desc_tip'    => true,
			),
			self::METHOD_FREE_MIN_AMOUNT     => array(
				'title'       => __( 'Minimum order amount', 'easy-shipping-rate' ),
				'type'        => 'price',
				'placeholder' => wc_format_localized_price( 0 ),
				'description' => __( 'Users will need to spend this amount to get free shipping (if enabled above).', 'easy-shipping-rate' ),
				'default'     => '0',
				'desc_tip'    => true,
			),
			self::METHOD_FREE_IGN_DISC       => array(
				'title'       => __( 'Coupons discounts', 'easy-shipping-rate' ),
				'label'       => __( 'Apply minimum order rule before coupon discount', 'easy-shipping-rate' ),
				'type'        => 'checkbox',
				'description' => __( 'If checked, free shipping would be available based on pre-discount order amount.', 'easy-shipping-rate' ),
				'default'     => 'no',
				'desc_tip'    => true,
			),
			self::METHOD_FREE_USER_POSTCODE  => array(
				'title'       => __( 'User Postcode', 'easy-shipping-rate' ),
				'type'        => 'text',
				'placeholder' => 'postcode1,postcode2,etc',
				'description' => __( 'Users with this Postal Code will get free shipping (if enabled above). To match several post codes separate them with commas', 'easy-shipping-rate' ),
				'desc_tip'    => true,
			),
			self::METHOD_FREE_NOTIFICATION   => array(
				'title'       => __( 'Free Shipping Notification', 'easy-shipping-rate' ),
				'type'        => 'checkbox',
				'description' => __( 'Let your customer know how much more to pay to get free shipping', 'easy-shipping-rate' ),
				'default'     => 'no',
				'desc_tip'    => true,
			),
			self::METHOD_FREE_SHIPPING_LABEL => array(
				'title'       => __( 'Free Shipping Label', 'easy-shipping-rate' ),
				'type'        => 'text',
				'default'     => __( 'Free', 'easy-shipping-rate' ),
				'description' => __( 'Enter free shipping label.', 'easy-shipping-rate' ),
				'desc_tip'    => true,
			),

			'section_advanced_options'       => array(
				'title' => __( 'Advanced Options', 'easy-shipping-rate' ),
				'type'  => 'title',
				'class' => 'esraw-text-2xl esraw-font-bold',
			),
			self::METHOD_VISIBILITY          => array(
				'title'   => __( 'Visibility', 'easy-shipping-rate' ),
				'type'    => 'checkbox',
				'default' => 'no',
				'label'   => 'Show only for logged in users.',
				// 'description' => __( 'Show only for logged in users.', 'easy-shipping-rate' ),
				// 'desc_tip' => true,
			),
			self::METHOD_DEFAULT             => array(
				'title'   => __( 'Default', 'easy-shipping-rate' ),
				'type'    => 'checkbox',
				'default' => 'no',
				'label'   => 'Set this Method as the default selected choice on the cart page.',
				// 'description' => __( 'Set this option as the default selected choice on the cart page.', 'easy-shipping-rate' ),
				// 'desc_tip' => true,
			),
			self::METHOD_MINIMUM_COST        => array(
				'title'       => __( 'Shipping minimum cost', 'easy-shipping-rate' ),
				'type'        => 'price',
				'label'       => ' ',
				'default'     => null,
				'description' => __( 'Shipping will never be cheaper than this, even if your rules calculate a lower amount.', 'easy-shipping-rate' ),
				'desc_tip'    => false,
			),
			self::METHOD_MAXIMUM_COST        => array(
				'title'       => __( 'Shipping maximum cost', 'easy-shipping-rate' ),
				'type'        => 'price',
				'label'       => ' ',
				'default'     => null,
				'description' => __( 'Shipping will never be more expensive than this, even if your rules calculate a higher amount.', 'easy-shipping-rate' ),
				'desc_tip'    => false,
			),
			self::METHOD_ESTIMATED_DELIVERY  => array(
				'title'       => __( 'Estimated days for delivery', 'easy-shipping-rate' ),
				'type'        => 'number',
				'default'     => '',
				'description' => __( 'Estimated days for delivery. If filled, user will be notified of its delivery date.', 'easy-shipping-rate' ),
				'desc_tip'    => false,
			),
			self::METHOD_RULE_CALCULATION    => array(
				'title'       => __( 'Rules Calculation', 'easy-shipping-rate' ),
				'type'        => 'select',
				'default'     => 'sum',
				'options'     => array(
					'sum'     => __( 'Sum', 'easy-shipping-rate' ),
					'lowest'  => __( 'Lowest cost', 'easy-shipping-rate' ),
					'highest' => __( 'Highest cost', 'easy-shipping-rate' ),
				),
				'description' => __( 'Applies when multiple rules all match via "Continue to next rule". Sum = total · Lowest = cheapest · Highest = priciest. Example: $5 + $3 → Sum $8 · Lowest $3 · Highest $5.', 'easy-shipping-rate' ),
				'desc_tip'    => false,
			),
			self::METHOD_DIM_FACTOR          => array(
				'title'       => __( 'DIM Factor', 'easy-shipping-rate' ),
				'type'        => 'number',
				'default'     => null,
				'description' => __( 'Used with the "Dimensional weight" condition. Dim. weight = Volume (cm³) ÷ DIM Factor — e.g. 9,000 cm³ ÷ 5,000 = 1.8 kg. Find the value on your carrier\'s rate card. All products must have dimensions set.', 'easy-shipping-rate' ),
				'desc_tip'    => false,
			),
			self::METHOD_DEBUG               => array(
				'title'       => __( 'Debug mode', 'easy-shipping-rate' ),
				'type'        => 'checkbox',
				'default'     => 'no',
				'label'       => __( 'Show a rule evaluation report on the cart and checkout (administrators only).', 'easy-shipping-rate' ),
				'description' => __( 'When enabled, administrators see how each rule was evaluated and why this shipping cost was produced. Customers never see it. You can also add ?debug_shipping=1 to the cart or checkout URL to switch it on temporarily and refresh the report.', 'easy-shipping-rate' ),
				'desc_tip'    => false,
			),
		);

		$this->instance_form_fields = $settings;

		$form_sets         = array(
			'config_general'         => array(
				'title'       => __( 'Plugin-level settings', 'easy-shipping-rate' ),
				'type'        => 'title',
				'class'       => 'esraw-text-2xl esraw-font-bold',
				'description' => __( 'These apply to all shipping zones and every Easy Shipping method — not just one zone or method.', 'easy-shipping-rate' ),
			),
			self::CONFIG_HIDE_ALL    => array(
				'title'       => __( 'Hide other methods when free shipping is available', 'easy-shipping-rate' ),
				'type'        => 'select',
				'default'     => '',
				'options'     => array(
					''         => __( 'N/A', 'easy-shipping-rate' ),
					'hide_all' => __( 'Show only "Free Shipping"', 'easy-shipping-rate' ),
				),
				'description' => __( 'When any Easy Shipping method qualifies for free shipping, all other shipping methods are hidden site-wide — this affects every shipping zone, not just the current one.', 'easy-shipping-rate' ),
				'desc_tip'    => false,
			),
			self::CONFIG_REMOVE_DATA => array(
				'title'       => __( 'Delete data on uninstall', 'easy-shipping-rate' ),
				'type'        => 'checkbox',
				'label'       => __( 'Permanently delete all settings and shipping rules when the plugin is deleted', 'easy-shipping-rate' ),
				'default'     => 'no',
				'description' => __( 'Leave unchecked to keep your shipping methods in case you reinstall the plugin later.', 'easy-shipping-rate' ),
				'desc_tip'    => false,
			),
		);
		$this->form_fields = $form_sets;
	}

	/**
	 * Return admin options as a html string.
	 *
	 * @return string
	 */
	public function get_admin_options_html() {
		if ( $this->instance_id ) {
			$settings_html = $this->instance_options();
		} else {
			$settings_html = $this->generate_settings_html( $this->get_form_fields() );
		}
		return '<table class="form-table">' . $settings_html . '</table>';
	}

	/**
	 * admin_options function.
	 */
	public function instance_options() {
		$today = gmdate( 'Y-m-d' );
		$start = $this->get_option( self::METHOD_START_DATE );
		$end   = $this->get_option( self::METHOD_END_DATE );
		if ( $start && $today < $start ) {
			?>
			<tr><td colspan="2" class="esraw-p-0 esraw-pb-3"><div class="notice notice-warning inline esraw-m-0"><p>
			<?php
			printf(
				/* translators: %s: start date in YYYY-MM-DD format */
				esc_html__( 'This method is currently inactive — it will become available from %s.', 'easy-shipping-rate' ),
				'<strong>' . esc_html( $start ) . '</strong>'
			);
			?>
			</p></div></td></tr>
			<?php
		} elseif ( $end && $today > $end ) {
			?>
			<tr><td colspan="2" class="esraw-p-0 esraw-pb-3"><div class="notice notice-warning inline esraw-m-0"><p>
			<?php
			printf(
				/* translators: %s: end date in YYYY-MM-DD format */
				esc_html__( 'This method is currently inactive — its end date (%s) has passed.', 'easy-shipping-rate' ),
				'<strong>' . esc_html( $end ) . '</strong>'
			);
			?>
			</p></div></td></tr>
			<?php
		}
			$this->generate_settings_html( $this->get_instance_form_fields() );
		?>
			<tr>
				<td colspan="2" class="esraw-pl-0">
				<h2 class="esraw-m-0 esraw-mb-3"><?php esc_html_e( 'Shipping Rules', 'easy-shipping-rate' ); ?></h2>
				<input type="hidden" name="esr_securite_nonce" value="<?php echo esc_attr( wp_create_nonce( 'esr-security' ) ); ?>"/>
					<div id="esraw-template-loader" class="esraw-m-0 esraw-mb-3 esraw-flex esraw-flex-wrap esraw-items-center esraw-gap-2">
						<label for="esraw-template-select"><?php esc_html_e( 'Load a template:', 'easy-shipping-rate' ); ?></label>
						<select id="esraw-template-select">
							<option value=""><?php esc_html_e( 'Choose a starter template…', 'easy-shipping-rate' ); ?></option>
							<?php foreach ( self::rule_templates() as $template_key => $template ) : ?>
								<option value="<?php echo esc_attr( $template_key ); ?>"><?php echo esc_html( $template['label'] ); ?></option>
							<?php endforeach; ?>
						</select>
						<button type="button" class="button" id="esraw-load-template"><?php esc_html_e( 'Load', 'easy-shipping-rate' ); ?></button>
						<span class="description"><?php esc_html_e( 'Fills the table with example rules you can adjust before saving. Nothing is saved until you click "Save changes".', 'easy-shipping-rate' ); ?></span>
					</div>
					<table id="esr-woo-table" class="widefat striped esraw-w-full esraw-table-fixed">
						<thead>
							<tr>
								<th>
									<input type="checkbox" id="esr_remove_all_tr" class="esr_remove_all_tr">
								</th>
								<th><?php esc_html_e( 'Condition', 'easy-shipping-rate' ); ?></th>
								<th><?php esc_html_e( 'Cost', 'easy-shipping-rate' ); ?></th>
								<th><?php esc_html_e( 'Action', 'easy-shipping-rate' ); ?></th>
								<?php if ( Admin::INLINE_DELETE_ENABLED ) : ?>
									<th><?php esc_html_e( 'Delete', 'easy-shipping-rate' ); ?></th>
								<?php endif; ?>
								<th><span class="screen-reader-text"><?php esc_html_e( 'Row actions', 'easy-shipping-rate' ); ?></span></th>
							</tr>
						</thead>
						<tfoot>
							<tr>
								<th colspan="2">
									<a href="#" class="button button-primary" id="esr-insert-new-row"><?php esc_html_e( 'Insert row', 'easy-shipping-rate' ); ?></a>
								</th>
								<th colspan="<?php echo Admin::INLINE_DELETE_ENABLED ? 4 : 3; ?>" class="esraw-text-right">
									<a href="#" class="button" id="esr-remove-rows"><?php esc_html_e( 'Delete selected', 'easy-shipping-rate' ); ?></a>
								</th>
							</tr>
						</tfoot>
						<tbody>
							<?php if ( empty( $this->conditions_options ) ) : ?>
								<tr class="esr-empty-state">
									<td colspan="<?php echo Admin::INLINE_DELETE_ENABLED ? 6 : 5; ?>" class="esraw-py-7 esraw-px-3 esraw-text-center esraw-text-gray-500">
										<?php esc_html_e( 'No rules yet. Click "Insert row" to add your first rule.', 'easy-shipping-rate' ); ?>
									</td>
								</tr>
							<?php endif; ?>
							<?php foreach ( $this->conditions_options as $key => $condition ) : ?>
								<tr  data-key="<?php echo esc_attr( $key ); ?>" >
									<td>
										<input type="checkbox" data-num="<?php echo esc_attr( $key ); ?>" class="esr_remove_tr">
									</td>
									<td>
										<div class="easy_rate_condition_content" id="easy_rate_condition_content_<?php echo esc_attr( $key ); ?>">
											<select id="easy_rate_condition_<?php echo esc_attr( $key ); ?>" name="easy_rate[<?php echo esc_attr( $key ); ?>][condition]">
												<option></option>
												<?php foreach ( self::CONDITION_CHOICES as $groupe => $choices ) : ?>
													<optgroup label="<?php echo esc_attr( $groupe ); ?>">
														<?php foreach ( $choices as $choice_name => $choice_value ) : ?>
															<option value="<?php echo esc_attr( $choice_value ); ?>" <?php ( $choice_value === $condition['condition'] ) ? print( 'selected' ) : ''; ?>>
																<?php echo esc_attr( $choice_name ); ?>
															</option>
														<?php endforeach; ?>
													</optgroup>
												<?php endforeach; ?>
											</select>
											<span class="easy_rate_operator_content" id="easy_rate_operator_content_<?php echo esc_attr( $key ); ?>">
												<select id="easy_rate_operator_<?php echo esc_attr( $key ); ?>" name="easy_rate[<?php echo esc_attr( $key ); ?>][operator]" required>
													<?php foreach ( self::OPERATOR as $op_key => $operat ) : ?>
														<option value="<?php echo esc_attr( $op_key ); ?>" <?php ( $op_key === $condition['operator'] ) ? print( 'selected' ) : ''; ?>>
															<?php echo esc_attr( $operat ); ?>
														</option>
													<?php endforeach; ?>
												</select>
												<?php if ( 'contains_shipping_class' === $condition['condition'] ) : ?>
													<select multiple name="easy_rate[<?php echo esc_attr( $key ); ?>][choices][]" required="" id="esraw_ship_class">
														<?php foreach ( self::ship_classes_select_field() as $choice_key => $choice_class_value ) : ?>
															<option value="<?php echo esc_attr( $choice_key ); ?>" <?php in_array( $choice_key, $condition['choices'], true ) ? print( 'selected' ) : ''; ?>>
																<?php echo esc_attr( $choice_class_value ); ?>
															</option>
														<?php endforeach; ?>
													</select>
												<?php elseif ( 'contains_product' === $condition['condition'] ) : ?>
													<select multiple class="esraw-product-select" name="easy_rate[<?php echo esc_attr( $key ); ?>][choices][]" required="">
														<?php
														foreach ( (array) $condition['choices'] as $product_id ) :
															$product = wc_get_product( absint( $product_id ) );
															if ( $product ) :
																?>
																<option value="<?php echo esc_attr( $product_id ); ?>" selected="selected">
																	<?php echo esc_html( $product->get_name() ); ?>
																</option>
																<?php
														endif;
endforeach;
														?>
													</select>
												<?php elseif ( 'user_roles' === $condition['condition'] ) : ?>
													<select multiple id="esraw_user_roles" name="easy_rate[<?php echo esc_attr( $key ); ?>][choices][]" required="">
														<?php foreach ( get_user_roles() as $choice_role_key => $choice_role_name ) : ?>
															<option value="<?php echo esc_attr( $choice_role_key ); ?>" <?php in_array( $choice_role_key, $condition['choices'], true ) ? print( 'selected' ) : ''; ?>>
																<?php echo esc_attr( $choice_role_name ); ?>
															</option>
														<?php endforeach; ?>
													</select>
												<?php elseif ( 'zipcode' === $condition['condition'] ) : ?>
													<input type="text" placeholder="<?php echo esc_attr( is_premium() ? '75001, 75*, 1000-2000' : '75001, 75002, etc.' ); ?>" name="easy_rate[<?php echo esc_attr( $key ); ?>][choices]" value="<?php echo esc_attr( $condition['choices'] ); ?>"/>
													<span class="description esraw-block esraw-text-[11px] esraw-mt-0.5"><?php esc_html_e( 'Not case-sensitive. Separate multiple values with a comma.', 'easy-shipping-rate' ); ?></span>
												<?php elseif ( 'city' === $condition['condition'] ) : ?>
													<input type="text" placeholder="city1,city2,etc." name="easy_rate[<?php echo esc_attr( $key ); ?>][choices]" value="<?php echo esc_attr( $condition['choices'] ); ?>"/>
													<span class="description esraw-block esraw-text-[11px] esraw-mt-0.5"><?php esc_html_e( 'Not case-sensitive. Separate multiple values with a comma.', 'easy-shipping-rate' ); ?></span>
												<?php elseif ( 'state' === $condition['condition'] ) : ?>
													<input type="text" placeholder="state1,state2,etc." name="easy_rate[<?php echo esc_attr( $key ); ?>][choices]" value="<?php echo esc_attr( $condition['choices'] ); ?>"/>
													<span class="description esraw-block esraw-text-[11px] esraw-mt-0.5"><?php esc_html_e( 'Not case-sensitive. Separate multiple values with a comma.', 'easy-shipping-rate' ); ?></span>
												<?php elseif ( 'country' === $condition['condition'] ) : ?>
													<input type="text" placeholder="country1,country2,etc." name="easy_rate[<?php echo esc_attr( $key ); ?>][choices]" value="<?php echo esc_attr( $condition['choices'] ); ?>"/>
													<span class="description esraw-block esraw-text-[11px] esraw-mt-0.5"><?php esc_html_e( 'Not case-sensitive. Separate multiple values with a comma.', 'easy-shipping-rate' ); ?></span>
												<?php else : ?>
													<span class="esraw-text-[11px] esraw-mr-[3px]"><?php esc_html_e( 'From', 'easy-shipping-rate' ); ?></span>
													<input type="number" step="0.01" value="<?php echo esc_attr( $condition['operand1'] ); ?>" placeholder="min" name="easy_rate[<?php echo esc_attr( $key ); ?>][operand1]"/>
													<span class="esraw-text-[11px] esraw-my-0 esraw-mx-[3px]"><?php esc_html_e( 'To', 'easy-shipping-rate' ); ?></span>
													<input type="number" step="0.01" value="<?php echo esc_attr( $condition['operand2'] ); ?>" placeholder="no limit" name="easy_rate[<?php echo esc_attr( $key ); ?>][operand2]"/>
												<?php endif; ?>
												<?php
													$unit = '';
												if ( 'subtotal' === $condition['condition'] || 'subtotal_ex' === $condition['condition'] ) {
													$unit = get_woocommerce_currency_symbol();
												} elseif ( 'quantity' === $condition['condition'] || 'cart_line_item' === $condition['condition'] ) {
													$unit = 'qty';
												} elseif ( 'weight' === $condition['condition'] || 'dim_weight' === $condition['condition'] ) {
													$unit = 'kg';
												} elseif ( 'volume' === $condition['condition'] ) {
													$unit = 'cm<sup>3</sup>';
												} elseif ( 'max_dim' === $condition['condition'] || 'total_dim' === $condition['condition'] ) {
													$unit = 'cm';
												}
												?>
												<div class="easy_rate_unit"><?php echo wp_kses_post( $unit ); ?> </div>
											</span>
										</div>
									</td>
									<td>
										<input type="text" class="esraw-w-full" value="<?php echo esc_attr( $condition['cost'] ); ?>" placeholder="<?php echo esc_attr( '12.50 or 5+[qty]*1.50' ); ?>" name="easy_rate[<?php echo esc_attr( $key ); ?>][cost]" required/>
									</td>
									<td>
										<select id="easy_rate_action_<?php echo esc_attr( $key ); ?>" name="easy_rate[<?php echo esc_attr( $key ); ?>][action]">
											<?php foreach ( self::CONDITIONS_ACTIONS as $ac_key => $action ) : ?>
												<option value="<?php echo esc_attr( $ac_key ); ?>" <?php ( isset( $condition['action'] ) && $ac_key === $condition['action'] ) ? print( 'selected' ) : ''; ?>>
													<?php echo esc_attr( $action ); ?>
												</option>
											<?php endforeach; ?>
										</select>
									</td>
									<?php if ( Admin::INLINE_DELETE_ENABLED ) : ?>
									<td>
										<button type="button" class="esr-remove-row" aria-label="<?php esc_attr_e( 'Delete this rule', 'easy-shipping-rate' ); ?>"><span class="dashicons dashicons-trash"></span></button>
									</td>
									<?php endif; ?>
									<td class="esr-row-actions esraw-whitespace-nowrap esraw-text-center">
										<button type="button" class="esr-duplicate-row esraw-inline-flex esraw-items-center esraw-justify-center esraw-rounded esraw-border-0 esraw-bg-transparent esraw-p-1.5 esraw-cursor-pointer esraw-text-gray-500 hover:esraw-text-brand hover:esraw-bg-gray-100" aria-label="<?php esc_attr_e( 'Duplicate this rule', 'easy-shipping-rate' ); ?>" title="<?php esc_attr_e( 'Duplicate', 'easy-shipping-rate' ); ?>">
											<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
										</button>
										<button type="button" class="esr-delete-row esraw-inline-flex esraw-items-center esraw-justify-center esraw-rounded esraw-border-0 esraw-bg-transparent esraw-p-1.5 esraw-cursor-pointer esraw-text-gray-500 hover:esraw-text-red-600 hover:esraw-bg-red-50" aria-label="<?php esc_attr_e( 'Delete this rule', 'easy-shipping-rate' ); ?>" title="<?php esc_attr_e( 'Delete', 'easy-shipping-rate' ); ?>">
											<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path><line x1="10" y1="11" x2="10" y2="17"></line><line x1="14" y1="11" x2="14" y2="17"></line></svg>
										</button>
									</td>
								</tr>
							<?php endforeach; ?>
						</tbody>
					</table>
					<p class="description">
						<?php esc_html_e( 'Cost accepts a fixed amount (e.g. 12.50) or a formula combining [qty], [items], [weight], [subtotal], [subtotal_ex] and [volume] with + - * / ( ). Examples: 5+[qty]*1.50 adds a per-item surcharge to a base fee; [subtotal]*0.08 charges 8% of the cart subtotal.', 'easy-shipping-rate' ); ?>
					</p>
					<p class="description">
						<?php esc_html_e( 'Rules run top to bottom. First match wins — unless its action is "Continue to next rule", which carries evaluation to the next row.', 'easy-shipping-rate' ); ?>
					</p>
					<?php if ( function_exists( 'esraw_fs' ) && \esraw_fs() && ! is_premium() ) : ?>
						<p class="description">
							<?php esc_html_e( 'Pro unlocks postcode ranges, combined (AND/OR) conditions and per-item cost tables.', 'easy-shipping-rate' ); ?>
							<a href="<?php echo esc_url( \esraw_fs()->get_upgrade_url() ); ?>"><?php esc_html_e( 'Upgrade to Pro', 'easy-shipping-rate' ); ?></a>
						</p>
					<?php endif; ?>
				</td>
			</tr>
		<?php
	}

	/**
	 * Match a customer postcode against a comma-separated list of patterns.
	 *
	 * Free: exact matches only (e.g. "75001,75002").
	 * Pro:  also unlocks wildcards ("75*") and numeric ranges ("1000-2000"),
	 *       gated behind esraw_is_premium() so the free build ignores them.
	 *
	 * @param string $postcode Customer shipping postcode.
	 * @param string $patterns Comma-separated patterns saved on the rule.
	 * @return bool
	 */
	private static function postcode_matches( $postcode, $patterns ) {
		$postcode = strtoupper( trim( (string) $postcode ) );
		$premium  = is_premium();

		foreach ( explode( ',', (string) $patterns ) as $pattern ) {
			$pattern = strtoupper( trim( $pattern ) );
			if ( '' === $pattern ) {
				continue;
			}

			// Exact match — the only behaviour available in the free version.
			if ( $postcode === $pattern ) {
				return true;
			}

			if ( ! $premium ) {
				continue;
			}

			// Pro: numeric range, e.g. "1000-2000".
			if ( preg_match( '/^(\d+)\s*-\s*(\d+)$/', $pattern, $range ) && ctype_digit( $postcode ) ) {
				if ( (int) $postcode >= (int) $range[1] && (int) $postcode <= (int) $range[2] ) {
					return true;
				}
				continue;
			}

			// Pro: wildcard, e.g. "75*" or "SW1*".
			if ( false !== strpos( $pattern, '*' ) ) {
				$regex = '/^' . str_replace( '\*', '.*', preg_quote( $pattern, '/' ) ) . '$/';
				if ( preg_match( $regex, $postcode ) ) {
					return true;
				}
			}
		}

		return false;
	}

	public static function ship_classes_select_field() {
		$shipping_classes     = WC()->shipping()->get_shipping_classes();
		$ship_classes_options = array();
		foreach ( $shipping_classes as $shipping_class ) {
			$ship_classes_options[ $shipping_class->slug ] = $shipping_class->name;
		}
		return $ship_classes_options;
	}

	/**
	 * Finds and returns shipping classes in cart
	 *
	 * @param mixed $package Package of items from cart.
	 * @return array
	 */
	public function find_shipping_classes( $package ) {
		$found_shipping_classes = array();

		foreach ( $package['contents'] as $values ) {
			if ( $values['data']->needs_shipping() ) {
				$found_class              = $values['data']->get_shipping_class();
				$found_shipping_classes[] = $found_class;
			}
		}

		return $found_shipping_classes;
	}

	/**
	 * Calculate_shipping function.
	 *
	 * @param mixed $package comment.
	 * @return void
	 */
	public function calculate_shipping( $package = array() ) {
		if ( ! $this->is_visible_for_user() ) {
			return;
		}
		$collect_debug = $this->debug_is_enabled();
		$debug_rules   = array();
		$debug_notes   = array();
		$cost          = 0;
		$label         = $this->get_translated_instance_option( self::METHOD_TITLE, 'Method title', $this->title );
		if ( $this->is_free_shipping ) {
			$cost  = 0;
			$label = $this->get_translated_instance_option( self::METHOD_FREE_SHIPPING_LABEL, 'Free shipping label', $this->title );
			if ( $collect_debug ) {
				$debug_notes[] = __( 'Free shipping is active — triggered by a coupon, postcode or minimum amount. The pricing rules below were not evaluated.', 'easy-shipping-rate' );
			}
		} else {
			$cost           = $this->get_instance_option( self::METHOD_MINIMUM_COST, 0 );
			$conditions_ops = $this->conditions_options;
			$matched_costs  = array();
			$rule_number    = 0;
			foreach ( $conditions_ops as $condition_row => $condition ) {
				++$rule_number;
				$array_compa    = false;
				$value_to_check = null;
				$debug_actual   = '';
				if ( 'subtotal' === $condition['condition'] ) {
					$value_to_check = $package['cart_subtotal'];
				} elseif ( 'subtotal_ex' === $condition['condition'] ) {
					$value_to_check = WC()->cart->get_subtotal();
				} elseif ( 'quantity' === $condition['condition'] ) {
					$value_to_check = WC()->cart->get_cart_contents_count();
				} elseif ( 'cart_line_item' === $condition['condition'] ) {
					$value_to_check = count( WC()->cart->get_cart() );
				} elseif ( 'weight' === $condition['condition'] ) {
					$value_to_check = WC()->cart->get_cart_contents_weight();
				} elseif ( 'volume' === $condition['condition'] ) {
					$value_to_check = get_cart_volume();
				} elseif ( 'dim_weight' === $condition['condition'] ) {
					$cart_vol = get_cart_volume();
					$dim_fact = $this->get_option( self::METHOD_DIM_FACTOR );
					if ( $dim_fact ) {
						$value_to_check = $cart_vol / $dim_fact;
					}
				}

				$can_get_cost = false;
				if ( 'contains_shipping_class' === $condition['condition'] ) {
					$find_ship_class = $this->find_shipping_classes( $package );
					$array_compa     = true;
					$debug_actual    = $find_ship_class ? implode( ', ', $find_ship_class ) : '—';
					if ( array_intersect( $find_ship_class, $condition['choices'] ) ) {
						$can_get_cost = true;
					}
				} elseif ( 'user_roles' === $condition['condition'] ) {
					if ( is_user_logged_in() ) {
						$user         = wp_get_current_user();
						$current_role = (array) $user->roles;
					} else {
						$current_role = 'not_login';
					}

					$array_compa  = true;
					$debug_actual = implode( ', ', (array) $current_role );
					if ( array_intersect( $current_role, $condition['choices'] ) ) {
						$can_get_cost = true;
					}
				} elseif ( 'zipcode' === $condition['condition'] ) {
					$customer_post_code = WC()->customer->get_shipping_postcode();

					$array_compa  = true;
					$debug_actual = $customer_post_code;
					if ( self::postcode_matches( $customer_post_code, $condition['choices'] ) ) {
						$can_get_cost = true;
					}
				} elseif ( 'city' === $condition['condition'] ) {
					$customer_city      = WC()->customer->get_shipping_city();
					$p_city_slice       = explode( ',', $condition['choices'] );
					$p_city_slice_lower = array_map(
						function ( $p ) {
							return strtolower( $p );
						},
						$p_city_slice
					);

					$array_compa  = true;
					$debug_actual = $customer_city;
					if ( in_array( strtolower( $customer_city ), $p_city_slice_lower, true ) ) {
						$can_get_cost = true;
					}
				} elseif ( 'state' === $condition['condition'] ) {
					$states                  = WC()->countries->get_states( WC()->customer->get_shipping_country() );
					$customer_state          = ! empty( $states[ WC()->customer->get_shipping_state() ] ) ? $states[ WC()->customer->get_shipping_state() ] : null;
						$p_state_slice       = explode( ',', $condition['choices'] );
						$p_state_slice_lower = array_map(
							function ( $p ) {
								return strtolower( $p );
							},
							$p_state_slice
						);
						$array_compa         = true;  // Flags conditions that use a list of choices instead of operand ranges.
						$debug_actual        = $customer_state;
					if ( in_array( strtolower( $customer_state ), $p_state_slice_lower, true ) ) {
						$can_get_cost = true;
					}
				} elseif ( 'country' === $condition['condition'] ) {
					$customer_country      = WC()->countries->countries[ WC()->customer->get_shipping_country() ];
					$p_country_slice       = explode( ',', $condition['choices'] );
					$p_country_slice_lower = array_map(
						function ( $p ) {
							return strtolower( $p );
						},
						$p_country_slice
					);

					$array_compa  = true;
					$debug_actual = $customer_country;
					if ( in_array( strtolower( $customer_country ), $p_country_slice_lower, true ) ) {
						$can_get_cost = true;
					}
				} elseif ( 'contains_product' === $condition['condition'] ) {
					$cart_items     = wc()->cart->get_cart();
					$cart_prod_ids  = array();
					foreach ( $cart_items as $item ) {
						$product_id = $item['product_id'];
						if ( isset( $item['variation_id'] ) && ! empty( $item['variation_id'] ) ) {
							$product_id = $item['variation_id'];
						}
						$cart_prod_ids[] = strval( $product_id );
						if ( in_array( strval( $product_id ), $condition['choices'], true ) ) {
							$can_get_cost = true;
						}
					}
					$array_compa  = true;
					$debug_actual = $cart_prod_ids ? implode( ', ', $cart_prod_ids ) : '—';
				}

				if ( ! $array_compa ) {
					if ( ( $condition['operand1'] <= $value_to_check || '' === $condition['operand1'] ) && ( $condition['operand2'] >= $value_to_check || '' === $condition['operand2'] ) ) {
						$can_get_cost = true;
					}
					$debug_actual = self::debug_scalar_actual( $condition['condition'], $value_to_check );
				}

				if ( 'is_not' === $condition['operator'] ) {
					$can_get_cost = ! $can_get_cost;
				}

				$rule_cost = null;
				if ( $can_get_cost ) {
					$rule_cost       = $this->resolve_rule_cost( $condition['cost'], $package );
					$matched_costs[] = $rule_cost;
				}

				if ( $collect_debug ) {
					$debug_rules[] = array(
						'row'     => $rule_number,
						'label'   => self::condition_human_label( $condition['condition'] ),
						'test'    => $this->debug_condition_detail( $condition ),
						'actual'  => (string) $debug_actual,
						'matched' => (bool) $can_get_cost,
						'cost'    => $can_get_cost ? wc_price( $rule_cost ) : null,
						'action'  => isset( $condition['action'] ) ? self::action_human_label( $condition['action'] ) : '',
					);
				}

				if ( isset( $condition['action'] ) ) {
					if ( 'stop' === $condition['action'] ) {
						break;
					} elseif ( 'cancel' === $condition['action'] ) {
						if ( $collect_debug ) {
							$this->store_debug_report(
								$debug_rules,
								$debug_notes,
								array( 'result' => __( 'This shipping method is hidden by a rule (Hide this shipping method action).', 'easy-shipping-rate' ) ),
								true
							);
						}
						return null;
					} elseif ( 'free_ship' === $condition['action'] ) {
						$cost          = 0;
						$matched_costs = array();
						$label         = $this->get_translated_instance_option( self::METHOD_FREE_SHIPPING_LABEL, 'Free shipping label', $this->title );
						break;
					}
				}
			}

			if ( ! empty( $matched_costs ) ) {
				$cost_calculation = $this->get_instance_option( self::METHOD_RULE_CALCULATION, 'sum' );
				if ( 'lowest' === $cost_calculation ) {
					$cost += min( $matched_costs );
				} elseif ( 'highest' === $cost_calculation ) {
					$cost += max( $matched_costs );
				} else {
					$cost += array_sum( $matched_costs );
				}
			}
		}
		$max_cost     = $this->get_instance_option( self::METHOD_MAXIMUM_COST, 0 );
		$max_cost_hit = ! empty( $max_cost ) && $cost > $max_cost;
		if ( $max_cost_hit ) {
			$cost = $max_cost;
		}

		if ( $collect_debug ) {
			$this->store_debug_report(
				$debug_rules,
				$debug_notes,
				$this->build_debug_summary( $cost, isset( $cost_calculation ) ? $cost_calculation : '', isset( $matched_costs ) ? $matched_costs : array(), $max_cost_hit ),
				false
			);
		}

		$rate = array(
			'label' => $label,
			'cost'  => $cost,
		);

		$this->add_rate( $rate );
	}

	/**
	 * Is method visible for user
	 *
	 * @return bool
	 */
	private function is_visible_for_user() {
		if ( 'yes' === $this->get_instance_option( self::METHOD_VISIBILITY, 'no' ) && ! is_user_logged_in() ) {
			return false;
		}
		return true;
	}

	/**
	 * See if free shipping is available based on the package and cart.
	 *
	 * @param array $package Shipping package.
	 * @return bool
	 */
	public function is_available( $package ) {
		$today_date = gmdate( 'Y-m-d' );
		$start_date = $this->get_option( self::METHOD_START_DATE, null );
		if ( $start_date && $today_date < $start_date ) {
			return;
		}

		$end_date = $this->get_option( self::METHOD_END_DATE, null );
		if ( $end_date && $end_date < $today_date ) {
			return;
		}

		$has_coupon         = false;
		$has_met_min_amount = false;

		if ( in_array( $this->requires, array( 'coupon', 'either', 'both' ), true ) ) {
			$coupons = WC()->cart->get_coupons();

			if ( $coupons ) {
				foreach ( $coupons as $coupon ) {
					if ( $coupon->is_valid() && $coupon->get_free_shipping() ) {
						$has_coupon = true;
						break;
					}
				}
			}
		}

		if ( in_array( $this->requires, array( 'min_amount', 'either', 'both' ), true ) ) {
			$total = WC()->cart->get_displayed_subtotal();

			if ( WC()->cart->display_prices_including_tax() ) {
				$total = $total - WC()->cart->get_discount_tax();
			}

			if ( 'no' === $this->ignore_discounts ) {
				$total = $total - WC()->cart->get_discount_total();
			}

			$total = NumberUtil::round( $total, wc_get_price_decimals() );

			if ( $total >= $this->min_amount ) {
				$has_met_min_amount = true;
			}
		}

		$has_met_postcode = false;
		if ( in_array( $this->requires, array( 'postcode' ), true ) ) {
			$customer_post_code = WC()->customer->get_shipping_postcode();
			$p_code_slice       = explode( ',', $this->postcode_need );

			if ( in_array( $customer_post_code, $p_code_slice, true ) ) {
				$has_met_postcode = true;
			}
		}

		switch ( $this->requires ) {
			case 'min_amount':
				$is_available = $has_met_min_amount;
				break;
			case 'coupon':
				$is_available = $has_coupon;
				break;
			case 'both':
				$is_available = $has_met_min_amount && $has_coupon;
				break;
			case 'either':
				$is_available = $has_met_min_amount || $has_coupon;
				break;
			case 'postcode':
				$is_available = $has_met_postcode;
				break;
			default:
				$is_available = false;
				break;
		}
		if ( $is_available ) {
			$this->is_free_shipping = true;
		} else {
			$is_available = parent::is_available( $package );
		}

		return apply_filters( 'woocommerce_shipping_' . $this->id . '_is_available', $is_available, $package, $this );
	}

	/**
	 * Enqueue JS to handle free shipping options.
	 *
	 * Static so that's registered only once regardless of how many instances exist.
	 */
	public static function enqueue_admin_js() {
		static $done = false;
		if ( $done ) {
			return;
		}
		$done = true;

		$js = "jQuery( function( $ ) {
			function wcFreeShippingShowHideMinAmountField( el ) {
				var form = $( el ).closest( 'form' );
				var minAmountField = $( '#woocommerce_esraw_method_free_min_amount', form ).closest( 'tr' );
				var ignoreDiscountField = $( '#woocommerce_esraw_method_free_ignore_discounts', form ).closest( 'tr' );
				var userPostcodeField = $( '#woocommerce_esraw_method_free_user_postcode', form ).closest( 'tr' );
				var freeNotificationField = $( '#woocommerce_esraw_method_free_shipping_notification', form ).closest( 'tr' );
				if ( 'coupon' === $( el ).val() || '' === $( el ).val() ) {
					minAmountField.hide();
					ignoreDiscountField.hide();
					userPostcodeField.hide();
					freeNotificationField.hide();
				} else {
					minAmountField.hide();
					ignoreDiscountField.hide();
					userPostcodeField.hide();
					freeNotificationField.hide();
					if ( 'postcode' === $( el ).val() ) {
						userPostcodeField.show();
					} else {
						minAmountField.show();
						freeNotificationField.show();
						if ( 'either' === $( el ).val() || 'both' === $( el ).val() ) {
							ignoreDiscountField.show();
						}
					}
				}
				checkMinAmountWarning();
			}

			function checkMinAmountWarning() {
				var amtField = $( '#woocommerce_esraw_method_free_min_amount' );
				$( '#esraw-min-amount-warning' ).remove();
				if ( amtField.closest( 'tr' ).is( ':visible' ) && ( parseFloat( amtField.val() ) === 0 || amtField.val() === '' ) ) {
					amtField.after( '<span id=\"esraw-min-amount-warning\" class=\"description esraw-text-[#d63638] esraw-block esraw-mt-1\">A value of 0 means free shipping will always be active for this method.</span>' );
				}
			}

			$( document.body ).on( 'change', '#woocommerce_esraw_method_free_requires', function() {
				wcFreeShippingShowHideMinAmountField( this );
			});

			$( document ).on( 'input change', '#woocommerce_esraw_method_free_min_amount', checkMinAmountWarning );

			function updateDeliveryPreview() {
				var field = $( '#woocommerce_esraw_method_estimated_delivery' );
				if ( ! field.length ) { return; }
				$( '#esraw-delivery-preview-wrap' ).remove();
				var days = parseInt( field.val(), 10 );
				if ( ! isNaN( days ) && days > 0 ) {
					var d = new Date();
					d.setDate( d.getDate() + days );
					var label = d.toLocaleDateString( undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' } );
					field.after( '<span class=\"description esraw-block esraw-mt-1\" id=\"esraw-delivery-preview-wrap\">With today\'s date, this would display: <em>' + label + '</em></span>' );
				}
			}
			$( document ).on( 'input change', '#woocommerce_esraw_method_estimated_delivery', updateDeliveryPreview );

			var req = $( '#woocommerce_esraw_method_free_requires' );
			if ( req.length ) {
				wcFreeShippingShowHideMinAmountField( req[0] );
			}
			updateDeliveryPreview();
		});";

		wp_register_script( 'esraw-method-settings', false, array( 'jquery' ), false, true );
		wp_enqueue_script( 'esraw-method-settings' );
		wp_add_inline_script( 'esraw-method-settings', $js );
	}

	/**
	 * Human-readable name of a condition value (e.g. "subtotal" → "Subtotal").
	 *
	 * @param string $value Stored condition slug.
	 * @return string
	 */
	public static function condition_human_label( $value ) {
		foreach ( self::CONDITION_CHOICES as $choices ) {
			$label = array_search( $value, $choices, true );
			if ( false !== $label ) {
				return $label;
			}
		}
		return $value;
	}

	/**
	 * Human-readable label of a rule action.
	 *
	 * @param string $action Stored action slug.
	 * @return string
	 */
	public static function action_human_label( $action ) {
		return isset( self::CONDITIONS_ACTIONS[ $action ] ) ? self::CONDITIONS_ACTIONS[ $action ] : $action;
	}

	/**
	 * Whether an administrator should see the rule evaluation report for this
	 * method: either its Debug mode is on, or an administrator asked for it with
	 * ?debug_shipping=1. Everything the report exposes stays admin-only.
	 *
	 * @return bool
	 */
	public function debug_is_enabled() {
		// The whole report is an administrator tool: never build or store it for
		// anyone else, so customer sessions are never touched.
		if ( ! function_exists( 'current_user_can' ) || ! current_user_can( 'manage_woocommerce' ) ) {
			return false;
		}
		if ( 'yes' === $this->get_instance_option( self::METHOD_DEBUG, 'no' ) ) {
			return true;
		}
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only, capability-gated display switch.
		return isset( $_GET['debug_shipping'] );
	}

	/**
	 * Unit shown next to a numeric condition value in the debug report.
	 *
	 * @param string $type Condition slug.
	 * @return string
	 */
	private static function debug_unit( $type ) {
		if ( 'subtotal' === $type || 'subtotal_ex' === $type ) {
			return get_woocommerce_currency_symbol();
		}
		if ( 'quantity' === $type || 'cart_line_item' === $type ) {
			return __( 'items', 'easy-shipping-rate' );
		}
		if ( 'weight' === $type || 'dim_weight' === $type ) {
			return 'kg';
		}
		if ( 'volume' === $type ) {
			return 'cm³';
		}
		return '';
	}

	/**
	 * Format the actual cart value a numeric condition was tested against.
	 *
	 * @param string $type  Condition slug.
	 * @param mixed  $value Cart value.
	 * @return string
	 */
	private static function debug_scalar_actual( $type, $value ) {
		if ( is_null( $value ) ) {
			return '—';
		}
		$unit = self::debug_unit( $type );
		return trim( $value . ' ' . $unit );
	}

	/**
	 * Describe what a rule tests, e.g. "is 0 – 50 kg" or "is not one of: FR, ES".
	 *
	 * @param array $condition Stored rule.
	 * @return string
	 */
	private function debug_condition_detail( $condition ) {
		$operator     = ( 'is_not' === $condition['operator'] ) ? __( 'is not', 'easy-shipping-rate' ) : __( 'is', 'easy-shipping-rate' );
		$type         = $condition['condition'];
		$choice_types = array( 'contains_shipping_class', 'user_roles', 'zipcode', 'city', 'state', 'country', 'contains_product' );

		if ( in_array( $type, $choice_types, true ) ) {
			$choices = is_array( $condition['choices'] ) ? implode( ', ', $condition['choices'] ) : $condition['choices'];
			/* translators: 1: is / is not, 2: list of expected values. */
			return trim( sprintf( __( '%1$s one of: %2$s', 'easy-shipping-rate' ), $operator, $choices ) );
		}

		$unit = self::debug_unit( $type );
		$from = ( '' === $condition['operand1'] || is_null( $condition['operand1'] ) ) ? '−∞' : $condition['operand1'];
		$to   = ( '' === $condition['operand2'] || is_null( $condition['operand2'] ) ) ? '∞' : $condition['operand2'];
		return trim( sprintf( '%s %s – %s %s', $operator, $from, $to, $unit ) );
	}

	/**
	 * Final-cost summary shown at the bottom of the debug report.
	 *
	 * @param float  $cost         Final cost after caps.
	 * @param string $calculation  Calculation mode used.
	 * @param array  $matched      Costs that matched.
	 * @param bool   $max_cost_hit Whether the maximum-cost cap was applied.
	 * @return array
	 */
	private function build_debug_summary( $cost, $calculation, $matched, $max_cost_hit ) {
		$modes = array(
			'sum'     => __( 'Sum', 'easy-shipping-rate' ),
			'lowest'  => __( 'Lowest cost', 'easy-shipping-rate' ),
			'highest' => __( 'Highest cost', 'easy-shipping-rate' ),
		);
		$summary = array(
			'matched_count' => count( $matched ),
			'final_cost'    => wc_price( $cost ),
		);
		if ( count( $matched ) > 1 ) {
			$summary['calculation'] = isset( $modes[ $calculation ] ) ? $modes[ $calculation ] : $calculation;
		}
		if ( $max_cost_hit ) {
			$summary['max_cost_hit'] = true;
		}
		return $summary;
	}

	/**
	 * Save this method's debug report in the customer session so the cart and
	 * checkout can render it for administrators. Keyed by instance so several
	 * methods can each keep their own report.
	 *
	 * @param array $rules   Per-rule evaluation entries.
	 * @param array $notes   Free-form notes (e.g. free shipping active).
	 * @param array $summary Final-cost summary.
	 * @param bool  $hidden  Whether the method ended up hidden.
	 */
	private function store_debug_report( $rules, $notes, $summary, $hidden ) {
		if ( is_null( WC()->session ) ) {
			return;
		}
		$reports = WC()->session->get( 'esraw_debug_reports', array() );
		$reports[ (string) $this->get_instance_id() ] = array(
			'method'  => $this->get_translated_instance_option( self::METHOD_TITLE, 'Method title', $this->title ),
			'rules'   => $rules,
			'notes'   => $notes,
			'summary' => $summary,
			'hidden'  => (bool) $hidden,
		);
		WC()->session->set( 'esraw_debug_reports', $reports );
	}
}
