<?php

/**
 * SCPWOO_Address
 *
 * Handles address formatting, saving, validation and restrictions
 * for the AfriRegions City & Neighborhood for WooCommerce plugin.
 *
 * @package SCPWOO
 */

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

class SCPWOO_Address {

    /**
     * Constructor — registers all address-related hooks.
     */
    public function __construct() {

        // ─── Neighborhood persistence (priority 10) ───

        add_action( 'woocommerce_checkout_create_order', array( $this, 'save_neighborhood_to_order' ), 10, 2 );
        add_action( 'woocommerce_admin_order_data_after_billing_address', array( $this, 'display_neighborhood_in_admin_billing' ), 10, 1 );
        add_action( 'woocommerce_admin_order_data_after_shipping_address', array( $this, 'display_neighborhood_in_admin_shipping' ), 10, 1 );
        add_filter( 'woocommerce_order_formatted_billing_address', array( $this, 'add_neighborhood_to_formatted_billing_address' ), 10, 2 );
        add_filter( 'woocommerce_order_formatted_shipping_address', array( $this, 'add_neighborhood_to_formatted_shipping_address' ), 10, 2 );
        add_filter( 'woocommerce_localisation_address_formats', array( $this, 'add_neighborhood_to_address_format' ), 10, 1 );
        add_filter( 'woocommerce_formatted_address_replacements', array( $this, 'replace_neighborhood_placeholder' ), 10, 2 );
        add_filter( 'woocommerce_my_account_my_address_formatted_address', array( $this, 'add_neighborhood_to_my_account_address' ), 10, 3 );


        add_filter( 'woocommerce_hidden_order_itemmeta', array( $this, 'hide_pickup_point_meta' ) );

        // ─── Force shipping calculation for supported countries ───
        add_filter( 'woocommerce_cart_ready_to_calc_shipping', array( $this, 'force_shipping_calculation' ) );

        // ─── Add neighborhood to shipping package ───
        // WC doesn't know about neighborhood. We inject it from post_data
        // into the package so calculate_shipping() can read it cleanly.
        add_filter( 'woocommerce_cart_shipping_packages', array( $this, 'add_neighborhood_to_package' ) );
        add_action( 'woocommerce_calculated_shipping', array( $this, 'save_calc_neighborhood_to_session' ) );

        // Force shipping recalculation when neighborhood changes
        // WC caches shipping rates by package hash — we must invalidate when neighborhood changes
        add_action( 'woocommerce_checkout_update_order_review', array( $this, 'invalidate_shipping_on_neighborhood_change' ) );

        // ─── Validation & zone restrictions (unified pipeline) ───

        add_action( 'woocommerce_after_checkout_validation', array( $this, 'validate_checkout' ), 10, 2 );
        add_action( 'woocommerce_check_cart_items', array( $this, 'check_cart_zone_restrictions' ) );
    }

    // =========================================================================
    // Neighborhood persistence
    // =========================================================================

    /**
     * Save neighborhood values to order metadata
     *
     * @param WC_Order $order Order object
     * @param array    $data  Checkout data
     */
    public function save_neighborhood_to_order( $order, array $data ): void {
        // Save billing neighborhood
        $billing_neighborhood = $data['billing_neighborhood'] ?? '';
        if ( ! empty( $billing_neighborhood ) ) {
            $order->update_meta_data( SCPWOO_States_Places::META_BILLING_NEIGHBORHOOD, sanitize_text_field( $billing_neighborhood ) );
        }

        // Save shipping neighborhood
        $shipping_neighborhood = $data['shipping_neighborhood'] ?? '';
        if ( ! empty( $shipping_neighborhood ) ) {
            $order->update_meta_data( SCPWOO_States_Places::META_SHIPPING_NEIGHBORHOOD, sanitize_text_field( $shipping_neighborhood ) );
        }
    }

    /**
     * Display billing neighborhood in WooCommerce admin
     *
     * @param WC_Order $order Order object
     */
    public function display_neighborhood_in_admin_billing( $order ): void {
        $neighborhood = $order->get_meta( SCPWOO_States_Places::META_BILLING_NEIGHBORHOOD );
        if ( ! empty( $neighborhood ) ) {
            echo '<p><strong>' . esc_html__( 'Neighborhood', 'afriregions-city-neighborhood-for-woocommerce' ) . ':</strong> ' . esc_html( $neighborhood ) . '</p>';
        }
    }

    /**
     * Display shipping neighborhood in WooCommerce admin
     *
     * @param WC_Order $order Order object
     */
    public function display_neighborhood_in_admin_shipping( $order ): void {
        $neighborhood = $order->get_meta( SCPWOO_States_Places::META_SHIPPING_NEIGHBORHOOD );
        if ( ! empty( $neighborhood ) ) {
            echo '<p><strong>' . esc_html__( 'Neighborhood', 'afriregions-city-neighborhood-for-woocommerce' ) . ':</strong> ' . esc_html( $neighborhood ) . '</p>';
        }
    }

    /**
     * Add neighborhood to formatted billing address
     *
     * @param array    $address Formatted address array
     * @param WC_Order $order   Order object
     * @return array
     */
    public function add_neighborhood_to_formatted_billing_address( array $address, $order ): array {
        $neighborhood = $order->get_meta( SCPWOO_States_Places::META_BILLING_NEIGHBORHOOD );
        $address['neighborhood'] = ! empty( $neighborhood ) ? $neighborhood : '';
        return $address;
    }

    /**
     * Add neighborhood to formatted shipping address
     *
     * @param array    $address Formatted address array
     * @param WC_Order $order   Order object
     * @return array
     */
    public function add_neighborhood_to_formatted_shipping_address( array $address, $order ): array {
        $neighborhood = $order->get_meta( SCPWOO_States_Places::META_SHIPPING_NEIGHBORHOOD );
        $address['neighborhood'] = ! empty( $neighborhood ) ? $neighborhood : '';
        return $address;
    }

    /**
     * Add the {neighborhood} placeholder to the address format
     *
     * @param array $formats Address formats array by country
     * @return array
     */
    public function add_neighborhood_to_address_format( array $formats ): array {
        $plugin = SCPWOO_States_Places::instance();
        if ( ! $plugin ) {
            return $formats;
        }

        $supported = $plugin->get_supported_countries();
        foreach ( $supported as $code ) {
            if ( isset( $formats[ $code ] ) ) {
                // Remove {address_2} — field is hidden for supported countries
                $formats[ $code ] = str_replace( array( "\n{address_2}", "{address_2}\n" ), '', $formats[ $code ] );
                // Insert {neighborhood} before {city} if not already present
                if ( strpos( $formats[ $code ], '{neighborhood}' ) === false ) {
                    $formats[ $code ] = str_replace( '{city}', "{neighborhood}\n{city}", $formats[ $code ] );
                }
            } else {
                $formats[ $code ] = "{name}\n{company}\n{address_1}\n{neighborhood}\n{city}\n{state}\n{country}";
            }
        }
        return $formats;
    }

    /**
     * Replace the {neighborhood} placeholder with the actual value in the formatted address
     *
     * @param array $replacements Replacements array
     * @param array $args         Arguments containing address values
     * @return array
     */
    public function replace_neighborhood_placeholder( array $replacements, array $args ): array {
        $replacements['{neighborhood}'] = isset( $args['neighborhood'] ) ? esc_html( $args['neighborhood'] ) : '';
        return $replacements;
    }

    /**
     * Add neighborhood to the address displayed on My Account > Addresses page
     *
     * @param array  $address      Address array
     * @param int    $customer_id  Customer ID
     * @param string $address_type Address type (billing or shipping)
     * @return array
     */
    public function add_neighborhood_to_my_account_address( array $address, int $customer_id, string $address_type ): array {
        $neighborhood = get_user_meta( $customer_id, $address_type . '_neighborhood', true );
        $address['neighborhood'] = ! empty( $neighborhood ) ? $neighborhood : '';
        return $address;
    }

    /**
     * Hide internal pickup point meta from the order items table in admin.
     *
     * @param array $hidden
     * @return array
     */
    public function hide_pickup_point_meta( array $hidden ): array {
        $hidden[] = '_scpwoo_pickup_points';
        $hidden[] = '_scpwoo_agency_points';
        return $hidden;
    }

    // =========================================================================
    // Validation
    // =========================================================================

    public function invalidate_shipping_on_neighborhood_change( string $post_data ): void {
        if ( ! WC()->session ) {
            return;
        }

        parse_str( $post_data, $data );
        $new_hood = ! empty( $data['billing_neighborhood'] ) ? $data['billing_neighborhood'] : '';
        if ( empty( $new_hood ) && ! empty( $data['shipping_neighborhood'] ) ) {
            $new_hood = $data['shipping_neighborhood'];
        }

        $prev_hood = WC()->session->get( 'scpwoo_last_neighborhood', null );

        // Skip on first call (no previous value stored yet)
        if ( $prev_hood === null ) {
            WC()->session->set( 'scpwoo_last_neighborhood', $new_hood );
            return;
        }

        if ( $new_hood !== $prev_hood ) {
            WC_Cache_Helper::get_transient_version( 'shipping', true );
            WC()->session->set( 'scpwoo_last_neighborhood', $new_hood );
        }
    }

    public function save_calc_neighborhood_to_session(): void {
        if ( ! WC()->session ) {
            return;
        }
        if ( isset( $_POST['calc_shipping_neighborhood'] ) ) {
            $val = sanitize_text_field( wp_unslash( $_POST['calc_shipping_neighborhood'] ) );
            if ( $val !== '' ) {
                WC()->session->set( 'calc_shipping_neighborhood', $val );
            } else {
                // User cleared the field — remove stale session value
                WC()->session->set( 'calc_shipping_neighborhood', '' );
            }
        }
    }

    /**
     * Add neighborhood to shipping package destination.
     *
     * Sources (in priority order):
     * 1. Cart shipping calculator POST
     * 2. Cart shipping calculator session (persisted)
     * 3. Checkout billing/shipping POST
     * 4. Checkout AJAX post_data
     *
     * @param array $packages
     * @return array
     */
    public function add_neighborhood_to_package( array $packages ): array {
        $neighborhood = '';

        // Source 1: checkout AJAX post_data (highest priority — user just changed it)
        // If post_data exists, we're in checkout context — don't fall back to session
        $is_checkout_ajax = ! empty( $_POST['post_data'] );
        if ( $is_checkout_ajax ) {
            parse_str( wp_unslash( $_POST['post_data'] ), $post_data );
            if ( ! empty( $post_data['shipping_neighborhood'] ) ) {
                $neighborhood = sanitize_text_field( $post_data['shipping_neighborhood'] );
            } elseif ( ! empty( $post_data['billing_neighborhood'] ) ) {
                $neighborhood = sanitize_text_field( $post_data['billing_neighborhood'] );
            }
            // If neighborhood is empty here, it means user hasn't selected one — respect that.
            // Do NOT fall through to session.
        }

        // Source 2: checkout direct POST (form submission, not AJAX)
        if ( ! $is_checkout_ajax && empty( $neighborhood ) && ! empty( $_POST['billing_neighborhood'] ) ) {
            $neighborhood = sanitize_text_field( wp_unslash( $_POST['billing_neighborhood'] ) );
        }

        // Source 3: cart shipping calculator POST
        if ( ! $is_checkout_ajax && empty( $neighborhood ) && ! empty( $_POST['calc_shipping_neighborhood'] ) ) {
            $neighborhood = sanitize_text_field( wp_unslash( $_POST['calc_shipping_neighborhood'] ) );
        }

        // Source 4: cart shipping calculator session (only when NOT in checkout context)
        // Log when falling back to session — a mismatch here can cause incorrect shipping rates
        if ( ! $is_checkout_ajax && empty( $neighborhood ) && WC()->session ) {
            $session_hood = WC()->session->get( 'calc_shipping_neighborhood' );
            if ( ! empty( $session_hood ) ) {
                SCPWOO_Audit::log( 'info', 'Neighborhood resolved from session fallback', array( 'neighborhood' => $session_hood ) );
                $neighborhood = $session_hood;
            }
        }

        if ( ! empty( $neighborhood ) ) {
            // Validate: neighborhood must belong to the current city
            $plugin = SCPWOO_States_Places::instance();
            foreach ( $packages as &$package ) {
                $pkg_city    = isset( $package['destination']['city'] ) ? $package['destination']['city'] : '';
                $pkg_country = isset( $package['destination']['country'] ) ? $package['destination']['country'] : '';

                if ( $pkg_city && $plugin ) {
                    // Resolve aliases before validation against known neighborhoods
                    $neighborhood = SCPWOO_Aliases::resolve_neighborhood( $neighborhood, $pkg_city, $pkg_country );

                    $country_hoods = $plugin->get_neighborhoods( $pkg_country );
                    if ( is_array( $country_hoods ) && isset( $country_hoods[ $pkg_city ] ) ) {
                        // Check if the neighborhood exists in the current city (case-insensitive)
                        $city_hoods_lower = array_map( 'mb_strtolower', $country_hoods[ $pkg_city ] );
                        if ( in_array( mb_strtolower( $neighborhood ), $city_hoods_lower, true ) ) {
                            $package['destination']['neighborhood'] = $neighborhood;
                        }
                        // else: neighborhood doesn't belong to this city — skip it
                    }
                } else {
                    // No city in package — inject anyway (let calculate_shipping decide)
                    $package['destination']['neighborhood'] = $neighborhood;
                }
            }
        }

        return $packages;
    }

    public function force_shipping_calculation( $ready ): bool {
        $plugin = SCPWOO_States_Places::instance();
        if ( ! $plugin || ! function_exists( 'WC' ) || ! WC()->customer ) {
            return $ready;
        }

        $country = WC()->customer->get_shipping_country();
        if ( ! $country ) {
            $country = WC()->customer->get_billing_country();
        }

        if ( $plugin->is_supported_country( $country ) ) {
            $state = WC()->customer->get_shipping_state();
            $city  = WC()->customer->get_shipping_city();
            if ( $state && $city ) {
                return true;
            }
        }

        return $ready;
    }

    /**
     * Unified checkout validation pipeline.
     * Runs address validation then zone restrictions in a single pass.
     *
     * @param array    $data   Checkout form data.
     * @param WP_Error $errors Errors object.
     */
    public function validate_checkout( array $data, $errors ): void {
        $plugin = SCPWOO_States_Places::instance();
        if ( ! $plugin ) {
            return;
        }

        // Step 1: Address validation
        $this->run_address_validation( $data, $errors, $plugin );

        // Step 2: Zone restrictions (only if no address errors — invalid address can't be checked for restrictions)
        if ( ! $this->has_validation_errors( $errors ) ) {
            $this->run_zone_restrictions( $data, $errors, $plugin );
        }
    }

    /**
     * Check whether the WP_Error object contains any address validation errors.
     *
     * @param WP_Error $errors Errors object.
     * @return bool
     */
    private function has_validation_errors( $errors ): bool {
        return in_array( 'validation', $errors->get_error_codes(), true );
    }

    /**
     * Run address field validation for billing and optionally shipping.
     *
     * @param array             $data   Checkout form data.
     * @param WP_Error          $errors Errors object.
     * @param SCPWOO_States_Places  $plugin Plugin instance.
     */
    private function run_address_validation( array $data, $errors, $plugin ): void {
        // Check if validation is enabled in admin
        $settings = SCPWOO_States_Places::get_json_option( SCPWOO_States_Places::OPTION_SETTINGS );
        $validation_enabled = $settings['address_validation_enabled'] ?? 'yes';
        if ( $validation_enabled !== 'yes' ) {
            return;
        }

        // Skip strict validation in degraded mode (JS/AJAX failed on client).
        // Read from WC session — never trust POST directly (client-side bypass vector).
        $degraded = WC()->session ? WC()->session->get( 'scpwoo_degraded_mode', false ) : false;
        if ( $degraded ) {
            WC()->session->set( 'scpwoo_degraded_mode', false ); // one-shot: clear after use
            SCPWOO_Audit::log( 'info', 'Checkout in degraded mode — strict address validation skipped' );
            return;
        }

        // Validate billing address
        if ( isset( $data['billing_country'] ) && $plugin->is_supported_country( $data['billing_country'] ) ) {
            $this->validate_address_fields( $data, $errors, 'billing' );
        }

        // Validate shipping address (if different)
        if ( isset( $data['ship_to_different_address'] ) && $data['ship_to_different_address'] ) {
            if ( isset( $data['shipping_country'] ) && $plugin->is_supported_country( $data['shipping_country'] ) ) {
                $this->validate_address_fields( $data, $errors, 'shipping' );
            }
        }
    }

    /**
     * Validate address fields for a given type (billing or shipping)
     *
     * @param array    $data   Form data
     * @param WP_Error $errors Errors object
     * @param string   $type   'billing' or 'shipping'
     */
    private function validate_address_fields( array $data, $errors, string $type ): void {
        $country = $data[ $type . '_country' ] ?? '';
        $state   = $data[ $type . '_state' ] ?? '';
        $city    = $data[ $type . '_city' ] ?? '';

        $label_type = $type === 'billing'
            ? __( 'billing', 'afriregions-city-neighborhood-for-woocommerce' )
            : __( 'shipping', 'afriregions-city-neighborhood-for-woocommerce' );

        // 1. Check that the region exists
        $plugin = SCPWOO_States_Places::instance();
        if ( ! $plugin ) {
            return;
        }

        if ( ! empty( $state ) ) {
            $places = $plugin->get_places( $country );
            if ( is_array( $places ) && ! isset( $places[ $state ] ) ) {
                SCPWOO_Audit::log_validation_failure( $type, 'invalid_region', array( 'country' => $country, 'state' => $state ) );
                $errors->add(
                    'validation',
                    sprintf(
                        __( 'The selected region for the %s address is not valid.', 'afriregions-city-neighborhood-for-woocommerce' ),
                        $label_type
                    )
                );
                return;
            }

            // Resolve city alias before validation
            $city = SCPWOO_Aliases::resolve_city( $city, $country );

            // 2. Check that the city belongs to the region
            if ( ! empty( $city ) && is_array( $places ) && isset( $places[ $state ] ) ) {
                if ( ! in_array( $city, $places[ $state ], true ) ) {
                    SCPWOO_Audit::log_validation_failure( $type, 'invalid_city', array( 'country' => $country, 'state' => $state, 'city' => $city ) );
                    $errors->add(
                        'validation',
                        sprintf(
                            __( 'The city "%1$s" does not belong to the selected region for the %2$s address.', 'afriregions-city-neighborhood-for-woocommerce' ),
                            esc_html( $city ),
                            $label_type
                        )
                    );
                    return;
                }
            }

            // 3. Check that the neighborhood belongs to the city (if selected from a dropdown)
            $neighborhood_key = $type . '_neighborhood';
            $neighborhood = isset( $data[ $neighborhood_key ] ) ? sanitize_text_field( $data[ $neighborhood_key ] ) : '';

            // Resolve neighborhood alias before validation
            $neighborhood = SCPWOO_Aliases::resolve_neighborhood( $neighborhood, $city, $country );

            if ( ! empty( $neighborhood ) && ! empty( $city ) ) {
                $country_neighborhoods = $plugin->get_neighborhoods( $country );
                // Only validate if the city has neighborhoods in the data (otherwise it's a free text field)
                if ( is_array( $country_neighborhoods ) && isset( $country_neighborhoods[ $city ] ) ) {
                    $hood_lower = mb_strtolower( trim( $neighborhood ) );
                    $valid = false;
                    foreach ( $country_neighborhoods[ $city ] as $valid_hood ) {
                        if ( mb_strtolower( trim( $valid_hood ) ) === $hood_lower ) {
                            $valid = true;
                            break;
                        }
                    }
                    if ( ! $valid ) {
                        SCPWOO_Audit::log_validation_failure( $type, 'invalid_neighborhood', array( 'country' => $country, 'city' => $city, 'neighborhood' => $neighborhood ) );
                        $errors->add(
                            'validation',
                            sprintf(
                                __( 'The neighborhood "%1$s" does not belong to the city "%2$s" for the %3$s address.', 'afriregions-city-neighborhood-for-woocommerce' ),
                                esc_html( $neighborhood ),
                                esc_html( $city ),
                                $label_type
                            )
                        );
                    }
                }
            }
        }
    }

    // =========================================================================
    // Zone restrictions
    // =========================================================================

    /**
     * Run zone restriction checks for billing and optionally shipping.
     *
     * @param array             $data   Checkout form data.
     * @param WP_Error          $errors Errors object.
     * @param SCPWOO_States_Places  $plugin Plugin instance.
     */
    private function run_zone_restrictions( array $data, $errors, $plugin ): void {
        $restrictions = SCPWOO_States_Places::get_json_option( SCPWOO_States_Places::OPTION_RESTRICTED_ZONES );
        if ( empty( $restrictions['enabled'] ) || $restrictions['enabled'] !== 'yes' ) {
            return;
        }

        $message = ! empty( $restrictions['message'] )
            ? $restrictions['message']
            : __( 'Sorry, we do not deliver to this area yet.', 'afriregions-city-neighborhood-for-woocommerce' );

        $restricted_regions = $restrictions['regions'] ?? array();
        $restricted_cities  = $restrictions['cities'] ?? array();

        // Check billing address
        if ( isset( $data['billing_country'] ) && $plugin->is_supported_country( $data['billing_country'] ) ) {
            $country = $data['billing_country'];
            $state   = $data['billing_state'] ?? '';
            $city    = $data['billing_city'] ?? '';

            if ( $this->is_zone_restricted( $country, $state, $city, $restricted_regions, $restricted_cities ) ) {
                $errors->add( 'restriction', esc_html( $message ) );
                return;
            }
        }

        // Check shipping address
        if ( isset( $data['ship_to_different_address'] ) && $data['ship_to_different_address'] ) {
            if ( isset( $data['shipping_country'] ) && $plugin->is_supported_country( $data['shipping_country'] ) ) {
                $country = $data['shipping_country'];
                $state   = $data['shipping_state'] ?? '';
                $city    = $data['shipping_city'] ?? '';

                if ( $this->is_zone_restricted( $country, $state, $city, $restricted_regions, $restricted_cities ) ) {
                    $errors->add( 'restriction', esc_html( $message ) );
                    return;
                }
            }
        }
    }

    /**
     * Also check restrictions at cart level (displays a notice)
     */
    public function check_cart_zone_restrictions(): void {
        $plugin = SCPWOO_States_Places::instance();
        if ( ! $plugin ) {
            return;
        }

        $restrictions = SCPWOO_States_Places::get_json_option( SCPWOO_States_Places::OPTION_RESTRICTED_ZONES );
        if ( empty( $restrictions['enabled'] ) || $restrictions['enabled'] !== 'yes' ) {
            return;
        }

        if ( ! function_exists( 'WC' ) || ! WC()->customer ) {
            return;
        }

        $country = WC()->customer->get_billing_country();
        if ( ! $plugin->is_supported_country( $country ) ) {
            return;
        }

        $state = WC()->customer->get_billing_state();
        $city  = WC()->customer->get_billing_city();

        if ( empty( $state ) && empty( $city ) ) {
            return;
        }

        $restricted_regions = $restrictions['regions'] ?? array();
        $restricted_cities  = $restrictions['cities'] ?? array();

        if ( $this->is_zone_restricted( $country, $state, $city, $restricted_regions, $restricted_cities ) ) {
            $message = ! empty( $restrictions['message'] )
                ? $restrictions['message']
                : __( 'Sorry, we do not deliver to this area yet.', 'afriregions-city-neighborhood-for-woocommerce' );

            wc_add_notice( esc_html( $message ), 'error' );
        }
    }

    /**
     * Check if a zone (region + city) is restricted.
     * Keys are stored as CC:RC for regions and CC:RC|City for cities.
     *
     * @param string $country_code  Two-letter country code (e.g. "CM")
     * @param string $state_code    Region code (e.g. "CE")
     * @param string $city
     * @param array  $restricted_regions
     * @param array  $restricted_cities
     * @return bool
     */
    private function is_zone_restricted( string $country_code, string $state_code, string $city, array $restricted_regions, array $restricted_cities ): bool {
        if ( empty( $country_code ) || empty( $state_code ) ) {
            return false;
        }

        $region_key = $country_code . ':' . $state_code;

        // Entire region restricted
        if ( in_array( $region_key, $restricted_regions, true ) ) {
            return true;
        }

        // Specific city restricted (format: "CC:RC|CityName")
        if ( ! empty( $city ) && in_array( $region_key . '|' . $city, $restricted_cities, true ) ) {
            return true;
        }

        return false;
    }
}
