<?php

defined( 'ABSPATH' ) || exit;
/**
 * Core exemption logic.
 *
 * Source-tag system:
 *   Every exemption has a source. When WooCommerce asks "is this user exempt?"
 *   we check ALL active sources. Exempt wins if any source says yes.
 *   This means this plugin never removes an exemption granted by another system.
 *
 * Sources:
 *   'admin_toggle'  — per-user "Always exempt" override on the user profile
 *   'role'          — user holds a role listed in the Tax-Exempt Roles setting
 *   'certificate'   — PRO: approved certificate with valid expiry date
 *   External sources set via rankd_tx_exemption_sources filter (e.g. wholesale)
 *
 *   The per-user "Never exempt" override hard-blocks all of the above.
 */
class Rankd_Tx_Exemption {
    /** @var array Static per-request cache: user_id => bool */
    private static $cache = array();

    /**
     * @var bool Per-request fallback marker when the WC session is unavailable.
     */
    private static $session_set_by_us = false;

    /**
     * Session key marking that the current vat-exempt=true value was granted
     * by THIS plugin. Persisted in the WC session so that on later requests
     * (e.g. after a checkout address change) the filter knows it may safely
     * re-evaluate our own grant, while still never touching a grant made by
     * another system.
     */
    const SESSION_GRANT_KEY = 'rankd_tx_granted';

    /**
     * Returns true if the active vat-exempt value was granted by this plugin.
     */
    private static function we_granted() {
        if ( self::$session_set_by_us ) {
            return true;
        }
        if ( isset( WC()->session ) && WC()->session ) {
            return (bool) WC()->session->get( self::SESSION_GRANT_KEY );
        }
        return false;
    }

    /**
     * Records (or clears) the our-grant marker.
     *
     * @param bool $granted
     */
    private static function mark_granted( $granted ) {
        self::$session_set_by_us = (bool) $granted;
        if ( isset( WC()->session ) && WC()->session ) {
            WC()->session->set( self::SESSION_GRANT_KEY, ( $granted ? 1 : 0 ) );
        }
    }

    public static function init() {
        // Push exemption into WC customer session as early as possible.
        add_action( 'woocommerce_load_cart_from_session', array(__CLASS__, 'set_customer_session') );
        // Filter fallback — covers REST, caching edge cases, and address
        // changes during checkout (jurisdiction re-evaluation).
        add_filter( 'woocommerce_customer_is_vat_exempt', array(__CLASS__, 'filter_vat_exempt'), 20 );
        // User Switching compatibility.
        // Both hooks pass the switched-TO user as the first argument.
        add_action( 'switch_to_user', array(__CLASS__, 'refresh_on_user_switch') );
        add_action( 'switch_back_to_user', array(__CLASS__, 'refresh_on_user_switch') );
        // Address-change re-evaluation — the same refresh User Switching
        // triggers, wired to the two places a customer changes jurisdiction:
        // the checkout form (fires on every field update via WC AJAX) and
        // the My Account address save. Works in BOTH directions: applies
        // exemption when the new address matches a certificate, and removes
        // it when it no longer matches any.
        add_action( 'woocommerce_checkout_update_order_review', array(__CLASS__, 'refresh_on_checkout_update') );
        add_action(
            'woocommerce_customer_save_address',
            array(__CLASS__, 'refresh_on_address_save'),
            10,
            2
        );
    }

    // -------------------------------------------------------------------------
    // Public API
    // -------------------------------------------------------------------------
    /**
     * Returns true if the user is tax exempt from ANY source.
     * Results are cached per request.
     *
     * @param  int  $user_id
     * @return bool
     */
    public static function is_exempt( $user_id ) {
        if ( isset( self::$cache[$user_id] ) ) {
            return self::$cache[$user_id];
        }
        $sources = self::get_active_sources( $user_id );
        $exempt = !empty( $sources );
        self::$cache[$user_id] = $exempt;
        return $exempt;
    }

    /**
     * Returns an array of active exemption source slugs for the user.
     * Empty array = not exempt.
     *
     * When $country is provided, the certificate source is matched to that
     * jurisdiction (Pro). Without it, any active certificate counts — used
     * for status display where no destination address applies.
     *
     * @param  int    $user_id
     * @param  string $country  Optional ISO country code for jurisdiction matching.
     * @param  string $state    Optional state/province code.
     * @return string[]
     */
    public static function get_active_sources( $user_id, $country = '', $state = '' ) {
        $sources = array();
        $override = Rankd_Tx_Capabilities::get_override( $user_id );
        // Hard block: "Never exempt" suppresses every source this plugin
        // manages — role rules, Pro certificates, and sources declared via
        // the rankd_tx_exemption_sources filter. (Grants other systems make
        // directly on the WC session are never touched — see we_granted().)
        if ( 'never' === $override ) {
            return array();
        }
        // Per-user override: Always exempt.
        // Kept as the 'admin_toggle' slug for continuity with stamped orders.
        if ( 'always' === $override ) {
            $sources[] = 'admin_toggle';
        }
        // Role-based exemption (settings: Tax-Exempt Roles).
        if ( Rankd_Tx_Capabilities::user_has_exempt_role( $user_id ) ) {
            $role_ok = true;
            // PRO: role exemption requires evidence — an approved certificate
            // covering this jurisdiction, or an owner override whose accepted
            // scope covers it. Free keeps the original trust-based behavior.
            if ( function_exists( 'rankd_tx_fs' ) && rankd_tx_fs()->can_use_premium_code__premium_only() ) {
                $role_ok = self::role_evidence_ok__premium_only( $user_id, $country, $state );
            }
            if ( $role_ok ) {
                $sources[] = 'role';
            }
        }
        // PRO: approved certificate source (jurisdiction-matched when address known).
        if ( function_exists( 'rankd_tx_fs' ) && rankd_tx_fs()->can_use_premium_code__premium_only() ) {
            $cert = self::get_active_certificate__premium_only( $user_id, $country, $state );
            if ( $cert ) {
                $sources[] = 'certificate';
            }
        }
        /**
         * Allow external systems to declare their own exemption sources.
         *
         * @param string[] $sources  Current active sources.
         * @param int      $user_id
         *
         * Example — wholesale plugin:
         *   add_filter( 'rankd_tx_exemption_sources', function( $sources, $user_id ) {
         *       if ( is_wholesale_exempt( $user_id ) ) $sources[] = 'wholesale';
         *       return $sources;
         *   }, 10, 2 );
         */
        $sources = apply_filters( 'rankd_tx_exemption_sources', $sources, $user_id );
        return $sources;
    }

    /**
     * Invalidates the per-request cache for a user.
     * Call after saving exemption changes so the same request sees fresh data.
     *
     * @param int $user_id
     */
    public static function bust_cache( $user_id ) {
        unset(self::$cache[$user_id]);
    }

    // -------------------------------------------------------------------------
    // WooCommerce hooks
    // -------------------------------------------------------------------------
    /**
     * Computes the live exemption decision for the current WC customer:
     * scope check, admin toggle, jurisdiction-matched certificate (Pro),
     * and external sources via the rankd_tx_exemption_sources filter.
     *
     * Used by both the session setter and the vat-exempt filter so the two
     * paths can never disagree.
     *
     * @param  int $user_id
     * @return bool
     */
    public static function compute_customer_exemption( $user_id, $country = null, $state = null ) {
        // is_in_scope() applies the rankd_tx_skip_user filter.
        if ( !$user_id || !Rankd_Tx_Capabilities::is_in_scope( $user_id ) ) {
            return false;
        }
        // Destination jurisdiction: explicit values win (address-change
        // re-evaluation passes the incoming address before WC has saved it);
        // otherwise read from the WC customer, shipping first.
        if ( null === $country ) {
            $country = '';
            $state = '';
            if ( isset( WC()->customer ) && WC()->customer instanceof WC_Customer ) {
                $country = WC()->customer->get_shipping_country();
                $state = WC()->customer->get_shipping_state();
                if ( !$country ) {
                    $country = WC()->customer->get_billing_country();
                    $state = WC()->customer->get_billing_state();
                }
            }
        }
        $state = (string) $state;
        // Admin toggle + jurisdiction-matched certificate (Pro) + external
        // sources. Exempt if ANY source is active.
        $sources = self::get_active_sources( $user_id, $country, $state );
        return !empty( $sources );
    }

    /**
     * Re-evaluates exemption when the checkout address changes.
     *
     * Fires on woocommerce_checkout_update_order_review (WC AJAX), which runs
     * BEFORE WooCommerce copies the posted fields onto the customer object —
     * so the incoming address is parsed from the serialized checkout form
     * data and passed to the computation explicitly. Cart totals are
     * recalculated by WooCommerce immediately after this action, so the tax
     * line reflects the new jurisdiction in the same AJAX response.
     *
     * @param string $post_data Serialized checkout form data.
     */
    public static function refresh_on_checkout_update( $post_data ) {
        if ( !is_user_logged_in() ) {
            return;
        }
        if ( !isset( WC()->customer ) || !WC()->customer instanceof WC_Customer ) {
            return;
        }
        parse_str( (string) $post_data, $data );
        // Respect "ship to a different address": jurisdiction follows the
        // shipping fields when checked, billing otherwise.
        $ship_elsewhere = !empty( $data['ship_to_different_address'] );
        $country = ( $ship_elsewhere ? sanitize_text_field( $data['shipping_country'] ?? '' ) : sanitize_text_field( $data['billing_country'] ?? '' ) );
        $state = ( $ship_elsewhere ? sanitize_text_field( $data['shipping_state'] ?? '' ) : sanitize_text_field( $data['billing_state'] ?? '' ) );
        if ( !$country ) {
            return;
            // No jurisdiction data in this update — nothing to re-evaluate.
        }
        // GUARD: WooCommerce posts the ENTIRE serialized checkout form on
        // every field update — email, phone, name, everything. Only act when
        // the destination jurisdiction actually differs from what the
        // customer object already holds; otherwise the page-load evaluation
        // already covered this state and touching the session again on every
        // keystroke is wasted work that risks interfering with WC's own
        // AJAX response handling.
        $current_country = ( WC()->customer->get_shipping_country() ?: WC()->customer->get_billing_country() );
        $current_state = ( WC()->customer->get_shipping_country() ? WC()->customer->get_shipping_state() : WC()->customer->get_billing_state() );
        if ( $country === $current_country && $state === (string) $current_state ) {
            return;
            // Jurisdiction unchanged — nothing to do.
        }
        $user_id = get_current_user_id();
        self::bust_cache( $user_id );
        $exempt = self::compute_customer_exemption( $user_id, $country, $state );
        // Both directions — but never remove a grant made by another system.
        if ( $exempt ) {
            self::mark_granted( true );
            WC()->customer->set_is_vat_exempt( true );
        } elseif ( self::we_granted() && WC()->customer->get_is_vat_exempt() ) {
            WC()->customer->set_is_vat_exempt( false );
            self::mark_granted( false );
        }
    }

    /**
     * Re-evaluates exemption when a customer saves a My Account address.
     *
     * @param int    $user_id      Customer whose address was saved.
     * @param string $load_address Address type: 'billing' or 'shipping'.
     */
    public static function refresh_on_address_save( $user_id, $load_address ) {
        if ( !isset( WC()->customer ) || !WC()->customer instanceof WC_Customer ) {
            return;
        }
        self::bust_cache( $user_id );
        // The address was just written to user meta; the customer object in
        // memory may be stale, so read the saved values directly. Shipping
        // takes precedence when present, matching checkout behavior.
        $country = (string) get_user_meta( $user_id, 'shipping_country', true );
        $state = (string) get_user_meta( $user_id, 'shipping_state', true );
        if ( !$country ) {
            $country = (string) get_user_meta( $user_id, 'billing_country', true );
            $state = (string) get_user_meta( $user_id, 'billing_state', true );
        }
        $exempt = ( '' !== $country ? self::compute_customer_exemption( $user_id, $country, $state ) : self::compute_customer_exemption( $user_id ) );
        if ( $exempt ) {
            self::mark_granted( true );
            WC()->customer->set_is_vat_exempt( true );
        } elseif ( self::we_granted() && WC()->customer->get_is_vat_exempt() ) {
            WC()->customer->set_is_vat_exempt( false );
            self::mark_granted( false );
        }
    }

    public static function set_customer_session() {
        if ( !is_user_logged_in() ) {
            return;
        }
        if ( !isset( WC()->customer ) || !WC()->customer instanceof WC_Customer ) {
            return;
        }
        $exempt = self::compute_customer_exemption( get_current_user_id() );
        if ( $exempt ) {
            self::mark_granted( true );
            WC()->customer->set_is_vat_exempt( true );
            return;
        }
        // Not exempt by our computation. If the persisted session value is a
        // grant WE made on an earlier request (e.g. before an address change),
        // withdraw it. A grant made by another system is never touched.
        if ( self::we_granted() && WC()->customer->get_is_vat_exempt() ) {
            WC()->customer->set_is_vat_exempt( false );
        }
        self::mark_granted( false );
    }

    public static function filter_vat_exempt( $exempt ) {
        // Another system granted exemption — hands off. We only re-evaluate
        // true values that originated from this plugin's own grant.
        if ( $exempt && !self::we_granted() ) {
            return true;
        }
        $user_id = get_current_user_id();
        if ( !$user_id ) {
            // Guests are never managed by this plugin; pass through whatever
            // value another system (or WooCommerce) provided.
            return $exempt;
        }
        $computed = self::compute_customer_exemption( $user_id );
        self::mark_granted( $computed );
        return $computed;
    }

    public static function refresh_on_user_switch( $user_id ) {
        if ( !isset( WC()->customer ) || !WC()->customer instanceof WC_Customer ) {
            return;
        }
        self::bust_cache( $user_id );
        $exempt = self::compute_customer_exemption( $user_id );
        self::mark_granted( $exempt );
        WC()->customer->set_is_vat_exempt( $exempt );
    }

}
