<?php
/**
 * Zone-based shipping method for AfriRegions City & Neighborhood for WooCommerce
 *
 * Cascade: Neighborhood → City → Region → Default
 * Most specific rule wins.
 *
 * @package AfriRegions
 */

defined( 'ABSPATH' ) or die( 'Forbidden' );

class SCPWOO_Shipping_RCN extends WC_Shipping_Method {

    private $all_regions       = array();
    private $all_places        = array();
    private $all_neighborhoods = array();
    private $regions_by_country       = array();
    private $places_by_country        = array();
    private $neighborhoods_by_country = array();
    private $allowed_countries        = array();
    private $rules_cache       = null;
    private $rules_by_type     = null;

    public function __construct( $instance_id = 0 ) {
        $this->id                 = 'scpwoo_zone_shipping';
        $this->instance_id        = absint( $instance_id );
        $this->method_title       = __( 'Zone Shipping', 'afriregions-city-neighborhood-for-woocommerce' );
        $this->method_description = __( 'Cascading shipping rates by region, city, or neighborhood.', 'afriregions-city-neighborhood-for-woocommerce' );
        $this->supports           = array( 'shipping-zones', 'instance-settings' );

        $this->load_geo_data();
        $this->init_form_fields();
        $this->init_settings();

        $this->title = $this->get_option( 'title', $this->method_title );

        add_action( 'woocommerce_update_options_shipping_' . $this->id, array( $this, 'process_admin_options' ) );
    }

    // =========================================================================
    // Geo data loading
    // =========================================================================

    private function load_geo_data() {
        $states = SCPWOO_Geo_Repository::get_states_by_country();
        $places = SCPWOO_Geo_Repository::get_merged_places_by_country();
        $hoods  = SCPWOO_Geo_Repository::get_merged_neighborhoods_by_country();

        $this->allowed_countries        = array_keys( $states );
        $this->regions_by_country       = $states;
        $this->places_by_country        = $places;
        $this->neighborhoods_by_country = $hoods;

        // Build flat merged arrays for backward compat
        $this->all_regions       = array();
        $this->all_places        = array();
        $this->all_neighborhoods = array();

        foreach ( $states as $cc => $regions ) {
            foreach ( $regions as $rc => $name ) {
                $this->all_regions[ $rc ] = $name;
            }
        }
        foreach ( $places as $cc => $regions ) {
            foreach ( $regions as $rc => $cities ) {
                $this->all_places[ $rc ] = isset( $this->all_places[ $rc ] )
                    ? array_merge( $this->all_places[ $rc ], $cities )
                    : $cities;
            }
        }
        foreach ( $hoods as $cc => $cities ) {
            foreach ( $cities as $city => $neighborhoods ) {
                $this->all_neighborhoods[ $city ] = $neighborhoods;
            }
        }
    }

    // =========================================================================
    // Form fields
    // =========================================================================

    public function init_form_fields() {
        $this->instance_form_fields = array(
            'title' => array(
                'title'   => __( 'Displayed title', 'afriregions-city-neighborhood-for-woocommerce' ),
                'type'    => 'text',
                'default' => __( 'Zone Shipping', 'afriregions-city-neighborhood-for-woocommerce' ),
            ),
            'default_rate' => array(
                'title'       => __( 'Default price', 'afriregions-city-neighborhood-for-woocommerce' ),
                'type'        => 'price',
                'default'     => '0',
                'desc_tip'    => true,
                'description' => __( 'Applied when no specific rule matches the destination.', 'afriregions-city-neighborhood-for-woocommerce' ),
            ),
            'default_free_from' => array(
                'title'       => __( 'Free shipping from', 'afriregions-city-neighborhood-for-woocommerce' ),
                'type'        => 'price',
                'default'     => '',
                'desc_tip'    => true,
                'description' => __( 'Cart total above which shipping is free (leave empty to disable).', 'afriregions-city-neighborhood-for-woocommerce' ),
            ),
            'hide_no_rule' => array(
                'title'       => __( 'Hide if no rule matches', 'afriregions-city-neighborhood-for-woocommerce' ),
                'type'        => 'checkbox',
                'default'     => 'no',
                'label'       => __( 'Hide this shipping method when no specific rule matches the destination.', 'afriregions-city-neighborhood-for-woocommerce' ),
                'desc_tip'    => true,
                'description' => __( 'When enabled, the method is hidden at checkout if no region, city, or neighborhood rule matches. When disabled, the default price applies.', 'afriregions-city-neighborhood-for-woocommerce' ),
            ),
            'shipping_rules_json' => array( 'type' => 'hidden', 'default' => '[]' ),
        );
    }

    // =========================================================================
    // Admin page
    // =========================================================================

    public function admin_options() {
        $rules    = $this->get_rules();
        $currency = get_woocommerce_currency_symbol();
        ?>
        <div id="scpwoo-wrap">

        <div class="scpwoo-shipping-header">
            <h2><?php echo esc_html( $this->method_title ); ?></h2>
            <div class="scpwoo-help-box" tabindex="0" role="button" aria-expanded="false">
                <span class="dashicons dashicons-editor-help scpwoo-help-icon" aria-hidden="true"></span>
                <span class="scpwoo-help-label"><?php esc_html_e( 'How does it work?', 'afriregions-city-neighborhood-for-woocommerce' ); ?></span>
                <div class="scpwoo-help-content">
                    <strong><?php esc_html_e( 'How to configure shipping rules', 'afriregions-city-neighborhood-for-woocommerce' ); ?></strong>
                    <ol>
                        <li><?php esc_html_e( 'Set a default price applied when no rule matches, or enable "Hide if no rule matches" to hide this method instead.', 'afriregions-city-neighborhood-for-woocommerce' ); ?></li>
                        <li><?php esc_html_e( 'Add rules by selecting a region, optionally a city, and optionally neighborhoods.', 'afriregions-city-neighborhood-for-woocommerce' ); ?></li>
                        <li><?php esc_html_e( 'Set the shipping price and optionally a cart total for free shipping.', 'afriregions-city-neighborhood-for-woocommerce' ); ?></li>
                        <li><?php esc_html_e( 'The most specific rule wins: Neighborhood > City > Region > Default.', 'afriregions-city-neighborhood-for-woocommerce' ); ?></li>
                    </ol>
                    <p class="scpwoo-help-note"><?php esc_html_e( 'Click the pencil icon to edit an existing rule. Changes are saved when you click "Save changes" below.', 'afriregions-city-neighborhood-for-woocommerce' ); ?></p>
                </div>
            </div>
        </div>

        <table class="form-table">
            <?php $this->generate_settings_html( array(
                'title'             => $this->instance_form_fields['title'],
                'hide_no_rule'      => $this->instance_form_fields['hide_no_rule'],
                'default_rate'      => $this->instance_form_fields['default_rate'],
                'default_free_from' => $this->instance_form_fields['default_free_from'],
            ) ); ?>
        </table>

        <h3 id="scpwoo-rules-heading" class="scpwoo-rules-title"><?php esc_html_e( 'Shipping rules', 'afriregions-city-neighborhood-for-woocommerce' ); ?></h3>
        <p class="scpwoo-rules-desc"><?php esc_html_e( 'Priority: Neighborhood > City > Region > Default.', 'afriregions-city-neighborhood-for-woocommerce' ); ?></p>

        <table id="scpwoo-tbl" aria-labelledby="scpwoo-rules-heading">
            <thead><tr>
                <th scope="col"><?php esc_html_e( 'Destination', 'afriregions-city-neighborhood-for-woocommerce' ); ?></th>
                <th scope="col" id="scpwoo-col-price" class="scpwoo-col-price"><?php echo esc_html( /* translators: %s = currency symbol */ sprintf( __( 'Price (%s)', 'afriregions-city-neighborhood-for-woocommerce' ), $currency ) ); ?></th>
                <th scope="col" id="scpwoo-col-free" class="scpwoo-col-free"><?php echo esc_html( /* translators: %s = currency symbol */ sprintf( __( 'Free shipping above (%s)', 'afriregions-city-neighborhood-for-woocommerce' ), $currency ) ); ?></th>
                <th scope="col" class="scpwoo-col-actions"><?php esc_html_e( 'Actions', 'afriregions-city-neighborhood-for-woocommerce' ); ?></th>
            </tr></thead>
            <tbody id="scpwoo-tbody" aria-live="polite"></tbody>
        </table>

        <!-- Add rule form -->
        <div class="scpwoo-add">
            <div class="scpwoo-add-row">
                <?php
                $multi_country = count( $this->allowed_countries ) > 1;
                $wc_countries  = function_exists( 'WC' ) && WC()->countries ? WC()->countries->get_countries() : array();
                $single_cc     = ! $multi_country && ! empty( $this->allowed_countries ) ? $this->allowed_countries[0] : '';
                ?>
                <div id="sc-add-country-wrap" <?php echo $multi_country ? '' : 'class="scpwoo-hidden"'; ?>>
                    <label for="sc-add-country"><?php esc_html_e( 'Country', 'afriregions-city-neighborhood-for-woocommerce' ); ?></label>
                    <select id="sc-add-country">
                        <?php if ( $multi_country ) : ?>
                            <option value=""><?php esc_html_e( 'Choose...', 'afriregions-city-neighborhood-for-woocommerce' ); ?></option>
                            <?php foreach ( $this->allowed_countries as $cc ) : ?>
                                <option value="<?php echo esc_attr( $cc ); ?>"><?php echo esc_html( isset( $wc_countries[ $cc ] ) ? $wc_countries[ $cc ] : $cc ); ?></option>
                            <?php endforeach; ?>
                        <?php else : ?>
                            <option value="<?php echo esc_attr( $single_cc ); ?>"><?php echo esc_html( isset( $wc_countries[ $single_cc ] ) ? $wc_countries[ $single_cc ] : $single_cc ); ?></option>
                        <?php endif; ?>
                    </select>
                </div>
                <div>
                    <label for="sc-add-region"><?php esc_html_e( 'Region', 'afriregions-city-neighborhood-for-woocommerce' ); ?></label>
                    <select id="sc-add-region">
                        <option value=""><?php esc_html_e( 'Choose...', 'afriregions-city-neighborhood-for-woocommerce' ); ?></option>
                        <?php if ( ! $multi_country ) :
                            $regions_for_single = isset( $this->regions_by_country[ $single_cc ] ) ? $this->regions_by_country[ $single_cc ] : $this->all_regions;
                            foreach ( $regions_for_single as $code => $name ) : ?>
                                <option value="<?php echo esc_attr( $code ); ?>"><?php echo esc_html( $name ); ?></option>
                            <?php endforeach;
                        endif; ?>
                    </select>
                </div>
                <div id="sc-add-city-wrap" class="scpwoo-hidden">
                    <label for="sc-add-city"><?php esc_html_e( 'City', 'afriregions-city-neighborhood-for-woocommerce' ); ?> <small class="scpwoo-label-hint">(<?php esc_html_e( 'optional', 'afriregions-city-neighborhood-for-woocommerce' ); ?>)</small></label>
                    <select id="sc-add-city" disabled><option value="">—</option></select>
                </div>
                <div>
                    <label for="sc-add-rate"><?php esc_html_e( 'Price', 'afriregions-city-neighborhood-for-woocommerce' ); ?></label>
                    <input type="number" id="sc-add-rate" min="0" step="any" />
                </div>
                <div>
                    <label for="sc-add-free"><?php esc_html_e( 'Free from', 'afriregions-city-neighborhood-for-woocommerce' ); ?></label>
                    <input type="number" id="sc-add-free" min="0" step="any" placeholder="<?php esc_attr_e( 'e.g. 25000', 'afriregions-city-neighborhood-for-woocommerce' ); ?>" />
                </div>
                <button type="button" class="button button-primary" id="sc-add-btn"><?php esc_html_e( 'Add', 'afriregions-city-neighborhood-for-woocommerce' ); ?></button>
                <button type="button" class="button scpwoo-hidden" id="sc-cancel-btn"><?php esc_html_e( 'Cancel', 'afriregions-city-neighborhood-for-woocommerce' ); ?></button>
            </div>
            <div id="sc-add-hoods-wrap" class="scpwoo-hidden scpwoo-hoods-wrap">
                <label class="scpwoo-hoods-label"><?php esc_html_e( 'Neighborhoods', 'afriregions-city-neighborhood-for-woocommerce' ); ?> <small class="scpwoo-label-hint">(<?php esc_html_e( 'optional — select to refine', 'afriregions-city-neighborhood-for-woocommerce' ); ?>)</small></label>
                <div class="scpwoo-hood-box" id="sc-add-hoods"></div>
            </div>
        </div>

        <input type="hidden" name="<?php echo esc_attr( $this->get_field_key( 'shipping_rules_json' ) ); ?>" id="scpwoo-json" value="<?php echo esc_attr( wp_json_encode( $rules ) ); ?>" />

        </div><!-- /#scpwoo-wrap -->

        <?php
        // Enqueue shipping rules JS — no inline script
        $cn = array();
        foreach ( $this->allowed_countries as $cc ) {
            $cn[ $cc ] = isset( $wc_countries[ $cc ] ) ? $wc_countries[ $cc ] : $cc;
        }

        wp_enqueue_script(
            'scpwoo-shipping-rules',
            plugin_dir_url( SCPWOO_PLUGIN_FILE ) . 'admin/js/scpwoo-shipping-rules.js',
            array( 'jquery', 'scpwoo-admin-js' ),
            SCPWOO_States_Places::VERSION,
            true
        );

        wp_localize_script( 'scpwoo-shipping-rules', 'scpwoo_shipping_params', array(
            'regions_by_country'       => $this->regions_by_country,
            'places_by_country'        => $this->places_by_country,
            'neighborhoods_by_country' => $this->neighborhoods_by_country,
            'countries'                => $this->allowed_countries,
            'country_names'            => $cn,
            'rules'                    => $rules,
            'currency'                 => $currency,
            'i18n'                     => array(
                'choose'           => __( 'Choose...', 'afriregions-city-neighborhood-for-woocommerce' ),
                'noRules'          => __( 'No rules — default rate applies.', 'afriregions-city-neighborhood-for-woocommerce' ),
                'add'              => __( 'Add', 'afriregions-city-neighborhood-for-woocommerce' ),
                'update'           => __( 'Update', 'afriregions-city-neighborhood-for-woocommerce' ),
                'cancel'           => __( 'Cancel', 'afriregions-city-neighborhood-for-woocommerce' ),
                'edit'             => __( 'Edit', 'afriregions-city-neighborhood-for-woocommerce' ),
                'invalidRate'      => __( 'Please enter a valid price.', 'afriregions-city-neighborhood-for-woocommerce' ),
                'duplicateRule'    => __( 'A rule for this destination already exists.', 'afriregions-city-neighborhood-for-woocommerce' ),
                'selectRegion'     => __( 'Please select a region and enter a price.', 'afriregions-city-neighborhood-for-woocommerce' ),
                'ruleAdded'        => __( 'Rule added.', 'afriregions-city-neighborhood-for-woocommerce' ),
                'ruleUpdated'      => __( 'Rule updated.', 'afriregions-city-neighborhood-for-woocommerce' ),
                'ruleDeleted'      => __( 'Rule deleted.', 'afriregions-city-neighborhood-for-woocommerce' ),
                'rulesCount'       => __( '%d rules', 'afriregions-city-neighborhood-for-woocommerce' ),
                'undo'             => __( 'Undo', 'afriregions-city-neighborhood-for-woocommerce' ),
                'filterHoods'      => __( 'Filter neighborhoods...', 'afriregions-city-neighborhood-for-woocommerce' ),
                'deleteRule'       => __( 'Delete rule', 'afriregions-city-neighborhood-for-woocommerce' ),
                'typeRegion'       => __( 'Region', 'afriregions-city-neighborhood-for-woocommerce' ),
                'typeCity'         => __( 'City', 'afriregions-city-neighborhood-for-woocommerce' ),
                'typeNeighborhood' => __( 'Neighborhood', 'afriregions-city-neighborhood-for-woocommerce' ),
                'alreadyUsed'      => __( 'already used in a rule', 'afriregions-city-neighborhood-for-woocommerce' ),
            ),
        ) );
        ?>
        <?php
    }

    /**
     * Override process_admin_options to handle shipping_rules_json ourselves,
     * because the parent sanitize_text_field() corrupts JSON before our validator runs.
     */
    public function process_admin_options() {
        if ( ! current_user_can( 'manage_woocommerce' ) ) {
            return;
        }

        // Let parent handle standard fields (title, default_rate, default_free_from).
        parent::process_admin_options();

        // Now handle shipping_rules_json from raw $_POST, bypassing WC's sanitize_text_field.
        $field_key = $this->get_field_key( 'shipping_rules_json' );
        // phpcs:ignore WordPress.Security.NonceVerification.Missing -- WC already verified nonce in parent call.
        $raw_json  = isset( $_POST[ $field_key ] ) ? wp_unslash( $_POST[ $field_key ] ) : '[]';
        $validated  = $this->validate_shipping_rules_json_field( 'shipping_rules_json', $raw_json );

        // Remove rules that reference deleted cities/neighborhoods
        $decoded = json_decode( $validated, true );
        if ( is_array( $decoded ) ) {
            $decoded   = $this->clean_orphan_rules( $decoded );
            $validated = wp_json_encode( $decoded );
        }

        $this->instance_settings['shipping_rules_json'] = $validated;

        update_option( $this->get_instance_option_key(), apply_filters(
            'woocommerce_shipping_' . $this->id . '_instance_settings_values',
            $this->instance_settings,
            $this
        ), 'yes' );
    }

    /**
     * Validate the JSON rules field — prevent WC sanitize_text_field from corrupting JSON.
     */
    public function validate_shipping_rules_json_field( $key, $value ) {
        $decoded = json_decode( $value, true );
        if ( ! is_array( $decoded ) ) {
            return '[]';
        }
        // Sanitize each rule
        $clean = array();
        $claimed_hoods = array(); // city_lower => array of hood_lower
        foreach ( $decoded as $rule ) {
            if ( ! is_array( $rule ) || empty( $rule['type'] ) || empty( $rule['region'] ) ) continue;
            $r = array(
                'type'      => sanitize_key( $rule['type'] ),
                'region'    => sanitize_text_field( $rule['region'] ),
                'rate'      => isset( $rule['rate'] ) ? floatval( $rule['rate'] ) : 0,
                'free_from' => ( isset( $rule['free_from'] ) && $rule['free_from'] !== '' ) ? floatval( $rule['free_from'] ) : '',
            );
            if ( ! empty( $rule['country'] ) ) {
                $r['country'] = sanitize_text_field( $rule['country'] );
            }
            if ( in_array( $r['type'], array( 'city', 'neighborhood' ), true ) && ! empty( $rule['city'] ) ) {
                $r['city'] = sanitize_text_field( $rule['city'] );
            }
            if ( $r['type'] === 'neighborhood' && ! empty( $rule['neighborhoods'] ) && is_array( $rule['neighborhoods'] ) ) {
                $r['neighborhoods'] = array_map( 'sanitize_text_field', $rule['neighborhoods'] );

                // Remove neighborhoods already claimed by a previous rule for the same city
                $city_lower = mb_strtolower( isset( $r['city'] ) ? $r['city'] : '' );
                if ( isset( $claimed_hoods[ $city_lower ] ) ) {
                    $r['neighborhoods'] = array_filter( $r['neighborhoods'], function( $h ) use ( $claimed_hoods, $city_lower ) {
                        return ! in_array( mb_strtolower( $h ), $claimed_hoods[ $city_lower ], true );
                    } );
                    $r['neighborhoods'] = array_values( $r['neighborhoods'] );
                    // Skip rule if all neighborhoods were duplicates
                    if ( empty( $r['neighborhoods'] ) ) continue;
                }
                // Track claimed neighborhoods
                if ( ! isset( $claimed_hoods[ $city_lower ] ) ) {
                    $claimed_hoods[ $city_lower ] = array();
                }
                foreach ( $r['neighborhoods'] as $h ) {
                    $claimed_hoods[ $city_lower ][] = mb_strtolower( $h );
                }
            }
            $clean[] = $r;
        }
        return wp_json_encode( $clean );
    }

    // =========================================================================
    // Calculate shipping (cascade: neighborhood → city → region → default)
    // =========================================================================

    public function calculate_shipping( $package = array() ) {
        $country    = isset( $package['destination']['country'] ) ? strtoupper( $package['destination']['country'] ) : '';
        $state_code = isset( $package['destination']['state'] ) ? $package['destination']['state'] : '';
        $city       = isset( $package['destination']['city'] ) ? $package['destination']['city'] : '';
        $cart_total = isset( $package['contents_cost'] ) ? floatval( $package['contents_cost'] ) : 0;

        if ( $this->is_zone_restricted( $country, $state_code, $city ) ) {
            return;
        }

        $this->get_rules(); // ensure rules + type index are loaded
        $matched_rule = null;

        // Neighborhood is injected into the package by SCPWOO_Address::add_neighborhood_to_package()
        // via the woocommerce_cart_shipping_packages filter.
        $neighborhood = isset( $package['destination']['neighborhood'] ) ? $package['destination']['neighborhood'] : '';

        // 1. Neighborhood rule (most specific) — uses type index
        if ( $city && $neighborhood ) {
            $hood_lower = mb_strtolower( trim( $neighborhood ) );
            $city_lower = mb_strtolower( trim( $city ) );
            foreach ( $this->get_rules_by_type( 'neighborhood' ) as $r ) {
                if ( $country && ! empty( $r['country'] ) && strtoupper( $r['country'] ) !== $country ) continue;
                if ( ! isset( $r['city'] ) || mb_strtolower( $r['city'] ) !== $city_lower ) continue;
                if ( ! isset( $r['neighborhoods'] ) || ! is_array( $r['neighborhoods'] ) ) continue;
                $hoods_lower = array_map( function( $h ) { return mb_strtolower( trim( $h ) ); }, $r['neighborhoods'] );
                if ( in_array( $hood_lower, $hoods_lower, true ) ) {
                    $matched_rule = $r;
                    break;
                }
            }
        }

        // 2. City rule
        if ( ! $matched_rule && $city ) {
            $city_lower = mb_strtolower( trim( $city ) );
            foreach ( $this->get_rules_by_type( 'city' ) as $r ) {
                if ( $country && ! empty( $r['country'] ) && strtoupper( $r['country'] ) !== $country ) continue;
                if ( isset( $r['city'] ) && mb_strtolower( $r['city'] ) === $city_lower ) {
                    $matched_rule = $r;
                    break;
                }
            }
        }

        // 3. Region rule
        if ( ! $matched_rule && $state_code ) {
            foreach ( $this->get_rules_by_type( 'region' ) as $r ) {
                if ( $country && ! empty( $r['country'] ) && strtoupper( $r['country'] ) !== $country ) continue;
                if ( strtoupper( $r['region'] ) === strtoupper( $state_code ) ) {
                    $matched_rule = $r;
                    break;
                }
            }
        }

        // 4. Default or hide
        if ( $matched_rule ) {
            $cost     = floatval( $matched_rule['rate'] );
            $free_thr = ( isset( $matched_rule['free_from'] ) && $matched_rule['free_from'] !== '' ) ? floatval( $matched_rule['free_from'] ) : null;
        } else {
            // No rule matched — hide method if option is enabled
            if ( $this->get_option( 'hide_no_rule', 'no' ) === 'yes' ) {
                return;
            }
            $cost     = floatval( $this->get_option( 'default_rate', 0 ) );
            $free_thr = $this->get_option( 'default_free_from', '' );
            $free_thr = ( $free_thr !== '' ) ? floatval( $free_thr ) : null;
        }

        // Emit the shipping rate
        $label = $this->title;

        // Check free threshold
        if ( $free_thr !== null && $cart_total >= $free_thr ) {
            $label .= ' — ' . __( 'Free', 'afriregions-city-neighborhood-for-woocommerce' );
            $cost = 0;
        }

        $this->add_rate( array(
            'id'    => $this->get_rate_id(),
            'label' => $label,
            'cost'  => $cost,
            'taxes' => false,
        ) );
    }

    // =========================================================================
    // Helpers
    // =========================================================================

    private function get_rules() {
        if ( $this->rules_cache !== null ) return $this->rules_cache;

        $json  = $this->get_option( 'shipping_rules_json', '[]' );
        $rules = json_decode( $json, true );

        if ( ! is_array( $rules ) ) {
            $rules = array();
        }

        // Migration from old formats — persist so it only runs once
        if ( empty( $rules ) ) {
            $rules = $this->migrate_old_rates();
            if ( ! empty( $rules ) ) {
                $this->instance_settings['shipping_rules_json'] = wp_json_encode( $rules );
                update_option( $this->get_instance_option_key(), $this->instance_settings, 'yes' );
                // Clean up old options
                $this->instance_settings = array_diff_key( $this->instance_settings, array_flip( array(
                    'city_rates_json', 'hood_rates_json', 'city_overrides',
                ) ) );
            }
        }

        $this->rules_cache = $rules;

        // Build type index for O(1) lookup in calculate_shipping
        $this->rules_by_type = array( 'neighborhood' => array(), 'city' => array(), 'region' => array() );
        foreach ( $rules as $r ) {
            $type = $r['type'] ?? '';
            if ( isset( $this->rules_by_type[ $type ] ) ) {
                $this->rules_by_type[ $type ][] = $r;
            }
        }

        return $this->rules_cache;
    }

    /**
     * Get rules filtered by type (uses pre-built index).
     *
     * @param string $type 'neighborhood', 'city', or 'region'.
     * @return array
     */
    private function get_rules_by_type( string $type ): array {
        if ( $this->rules_by_type === null ) {
            $this->get_rules();
        }
        return $this->rules_by_type[ $type ] ?? array();
    }

    /**
     * Remove rules that reference cities or neighborhoods no longer in the geo data.
     * Called on admin save to keep the rules clean.
     *
     * @param array $rules
     * @return array Cleaned rules.
     */
    private function clean_orphan_rules( array $rules ): array {
        $clean = array();
        foreach ( $rules as $r ) {
            $cc   = $r['country'] ?? '';
            $rc   = $r['region'] ?? '';
            $type = $r['type'] ?? '';

            // Region rules: check region exists
            if ( $type === 'region' ) {
                if ( isset( $this->regions_by_country[ $cc ][ $rc ] ) || isset( $this->all_regions[ $rc ] ) ) {
                    $clean[] = $r;
                }
                continue;
            }

            // City rules: check city exists in geo data
            if ( $type === 'city' ) {
                $city = $r['city'] ?? '';
                $places = $this->places_by_country[ $cc ][ $rc ] ?? ( $this->all_places[ $rc ] ?? array() );
                if ( in_array( $city, $places, true ) ) {
                    $clean[] = $r;
                }
                continue;
            }

            // Neighborhood rules: check city + at least one neighborhood still exists
            if ( $type === 'neighborhood' ) {
                $city  = $r['city'] ?? '';
                $hoods = $r['neighborhoods'] ?? array();
                $known = $this->neighborhoods_by_country[ $cc ][ $city ] ?? ( $this->all_neighborhoods[ $city ] ?? array() );
                if ( ! empty( $known ) ) {
                    $known_lower = array_map( 'mb_strtolower', $known );
                    $valid_hoods = array_filter( $hoods, function( $h ) use ( $known_lower ) {
                        return in_array( mb_strtolower( $h ), $known_lower, true );
                    } );
                    if ( ! empty( $valid_hoods ) ) {
                        $r['neighborhoods'] = array_values( $valid_hoods );
                        $clean[] = $r;
                    }
                }
                continue;
            }

            $clean[] = $r;
        }
        return $clean;
    }

    private function migrate_old_rates() {
        $rules = array();

        // Helper: find country for a region code in loaded data
        $find_country = function( $region_code ) {
            foreach ( $this->regions_by_country as $cc => $regions ) {
                if ( isset( $regions[ $region_code ] ) ) {
                    return $cc;
                }
            }
            // Fallback to first allowed country
            return ! empty( $this->allowed_countries ) ? $this->allowed_countries[0] : '';
        };

        // Migrate city rates
        $city_json = $this->get_option( 'city_rates_json', '[]' );
        $city_rates = json_decode( $city_json, true );
        if ( is_array( $city_rates ) ) {
            foreach ( $city_rates as $cr ) {
                if ( ! empty( $cr['city'] ) ) {
                    $region = isset( $cr['region'] ) ? $cr['region'] : '';
                    $rules[] = array(
                        'type'      => 'city',
                        'country'   => $find_country( $region ),
                        'region'    => $region,
                        'city'      => $cr['city'],
                        'rate'      => floatval( $cr['rate'] ),
                        'free_from' => '',
                    );
                }
            }
        }

        // Migrate hood rates (each as individual neighborhood rule)
        $hood_json = $this->get_option( 'hood_rates_json', '[]' );
        $hood_rates = json_decode( $hood_json, true );
        if ( is_array( $hood_rates ) ) {
            foreach ( $hood_rates as $hr ) {
                if ( ! empty( $hr['neighborhood'] ) ) {
                    $region = isset( $hr['region'] ) ? $hr['region'] : '';
                    $rules[] = array(
                        'type'          => 'neighborhood',
                        'country'       => $find_country( $region ),
                        'region'        => $region,
                        'city'          => $hr['city'],
                        'neighborhoods' => array( $hr['neighborhood'] ),
                        'rate'          => floatval( $hr['rate'] ),
                        'free_from'     => '',
                    );
                }
            }
        }

        // Also try old text-based city_overrides
        if ( empty( $rules ) ) {
            $raw = $this->get_option( 'city_overrides', '' );
            if ( ! empty( $raw ) ) {
                foreach ( explode( "\n", $raw ) as $line ) {
                    $line = trim( $line );
                    if ( empty( $line ) ) continue;
                    $sep = strpos( $line, '|' ) !== false ? '|' : ( strpos( $line, ':' ) !== false ? ':' : null );
                    if ( $sep === null ) continue;
                    $parts = explode( $sep, $line, 2 );
                    $city  = trim( $parts[0] );
                    if ( empty( $city ) ) continue;
                    $region  = '';
                    $country = '';
                    foreach ( $this->places_by_country as $cc => $country_places ) {
                        foreach ( $country_places as $rc => $cities ) {
                            if ( in_array( $city, $cities, true ) ) {
                                $region  = $rc;
                                $country = $cc;
                                break 2;
                            }
                        }
                    }
                    $rules[] = array(
                        'type'      => 'city',
                        'country'   => $country,
                        'region'    => $region,
                        'city'      => $city,
                        'rate'      => floatval( trim( $parts[1] ) ),
                        'free_from' => '',
                    );
                }
            }
        }

        return $rules;
    }

    private function is_zone_restricted( $country, $state_code, $city ) {
        $r = SCPWOO_States_Places::get_json_option( SCPWOO_States_Places::OPTION_RESTRICTED_ZONES );
        if ( empty( $r['enabled'] ) || $r['enabled'] !== 'yes' ) return false;
        $regions = isset( $r['regions'] ) ? $r['regions'] : array();
        $cities  = isset( $r['cities'] ) ? $r['cities'] : array();

        // Composite key: CC:RC
        $region_key = $country . ':' . $state_code;

        // Check composite key first, then legacy (plain RC) for backward compat
        if ( $state_code && ( in_array( $region_key, $regions, true ) || in_array( $state_code, $regions, true ) ) ) {
            return true;
        }
        if ( $state_code && $city ) {
            if ( in_array( $region_key . '|' . $city, $cities, true ) || in_array( $state_code . '|' . $city, $cities, true ) ) {
                return true;
            }
        }
        return false;
    }

}

