<?php
/**
 * Tax Calculation Class
 * Handles all tax-related calculations for GGM Commerce
 */

// If this file is called directly, abort.
if (!defined('ABSPATH')) {
    exit;
}

// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
// phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
// phpcs:disable PluginCheck.Security.DirectDB.UnescapedDBParameter

class GGM_Commerce_Tax {

    private static $instance = null;
    private $settings = null;

    /** @var array Per-request cache for tax rates by ID */
    private static $tax_rate_cache = [];

    /** @var array Per-request cache for product tax rate IDs */
    private static $product_tax_rate_cache = [];

    /** @var object|null Per-request cache for default tax rate */
    private static $default_tax_rate_cache = null;
    private static $default_tax_rate_loaded = false;

    /**
     * Get singleton instance
     */
    public static function get_instance() {
        if (self::$instance === null) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    /**
     * Constructor
     */
    public function __construct() {
        $this->load_settings();
    }

    /**
     * Load tax settings from database
     */
    private function load_settings() {
        $this->settings = get_option('ggm_commerce_tax_options', []);
    }

    /**
     * Reload settings (useful after settings change)
     */
    public function reload_settings() {
        $this->load_settings();
    }

    /**
     * Check if tax is enabled
     */
    public function is_enabled() {
        return !empty($this->settings['enabled']);
    }

    /**
     * Get tax display mode
     * Without B2B Premium: Always returns 'inclusive' (B2C default)
     * Without admin VAT number: Always returns 'inclusive' (cannot show ex-VAT prices)
     * With B2B Premium + VAT number: Returns the admin's setting
     *
     * @return string 'inclusive' or 'exclusive'
     */
    public function get_display_mode() {
        // Without B2B Premium activated, always use inclusive mode (B2C default)
        if (!$this->is_b2b_premium_activated()) {
            return 'inclusive';
        }

        // Without admin VAT number, cannot show prices excluding VAT
        if (!$this->admin_has_vat_number()) {
            return 'inclusive';
        }

        // With B2B Premium + VAT number, respect the admin's setting (default to inclusive)
        return isset($this->settings['display_mode']) ? $this->settings['display_mode'] : 'inclusive';
    }

    /**
     * Check if prices include tax
     */
    public function prices_include_tax() {
        return $this->get_display_mode() === 'inclusive';
    }

    /**
     * Get default tax rate (as decimal, e.g., 0.20 for 20%)
     */
    public function get_rate() {
        $rate = isset($this->settings['rate']) ? floatval($this->settings['rate']) : 0;
        return $rate / 100; // Convert percentage to decimal
    }

    /**
     * Get tax rate as percentage
     */
    public function get_rate_percentage() {
        return isset($this->settings['rate']) ? floatval($this->settings['rate']) : 0;
    }

    /**
     * Get tax label (VAT, GST, Tax, etc.)
     */
    public function get_label() {
        return isset($this->settings['label']) ? $this->settings['label'] : 'Tax';
    }

    /**
     * Get rounding mode
     * @return string 'line' or 'subtotal'
     */
    public function get_rounding_mode() {
        return isset($this->settings['rounding']) ? $this->settings['rounding'] : 'line';
    }

    /**
     * Check if B2B tax exemption is enabled
     * Requires both admin setting enabled AND premium feature activated
     */
    public function is_b2b_enabled() {
        // Check if B2B setting is enabled in tax settings
        if (empty($this->settings['b2b_enabled'])) {
            return false;
        }

        // Check if premium feature is activated
        if (function_exists('ggm_commerce_license')) {
            $license = ggm_commerce_license();
            if (!$license->is_widget_activated('b2b-tax-exemption')) {
                return false;
            }
        }

        return true;
    }

    /**
     * Check if B2B premium feature is activated (regardless of admin setting)
     */
    public function is_b2b_premium_activated() {
        if (function_exists('ggm_commerce_license')) {
            $license = ggm_commerce_license();
            return $license->is_widget_activated('b2b-tax-exemption');
        }
        return false;
    }

    /**
     * Check if B2B validation is required
     */
    public function is_b2b_validation_required() {
        return !empty($this->settings['b2b_require_validation']);
    }

    /**
     * Get allowed account types setting
     * Returns 'both', 'individual_only', or 'business_only'
     */
    public function get_allowed_account_types() {
        if (!$this->is_b2b_enabled()) {
            return 'individual_only'; // No B2B = individual only
        }
        return isset($this->settings['b2b_account_types']) ? $this->settings['b2b_account_types'] : 'both';
    }

    /**
     * Check if business account registration is allowed
     */
    public function is_business_registration_allowed() {
        $types = $this->get_allowed_account_types();
        return $types === 'both' || $types === 'business_only';
    }

    /**
     * Check if individual account registration is allowed
     */
    public function is_individual_registration_allowed() {
        $types = $this->get_allowed_account_types();
        return $types === 'both' || $types === 'individual_only';
    }

    /**
     * Check if account type selector should be shown on registration
     */
    public function should_show_account_type_selector() {
        return $this->get_allowed_account_types() === 'both';
    }

    /**
     * Calculate tax for a given amount
     *
     * @param float $amount The amount to calculate tax on
     * @param bool $price_includes_tax Whether the amount already includes tax
     * @param float|null $custom_rate Optional custom tax rate (as decimal)
     * @return array ['tax' => tax_amount, 'net' => net_amount, 'gross' => gross_amount]
     */
    public function calculate($amount, $price_includes_tax = null, $custom_rate = null) {
        if (!$this->is_enabled()) {
            return [
                'tax' => 0,
                'net' => $amount,
                'gross' => $amount,
            ];
        }

        // Use provided values or defaults
        $includes_tax = $price_includes_tax !== null ? $price_includes_tax : $this->prices_include_tax();
        $rate = $custom_rate !== null ? $custom_rate : $this->get_rate();

        if ($rate <= 0) {
            return [
                'tax' => 0,
                'net' => $amount,
                'gross' => $amount,
            ];
        }

        if ($includes_tax) {
            // Price includes tax - extract it
            // If price is 120 and rate is 0.20 (20%), net = 120 / 1.20 = 100, tax = 20
            $net = $amount / (1 + $rate);
            $tax = $amount - $net;
            $gross = $amount;
        } else {
            // Price excludes tax - add it
            // If price is 100 and rate is 0.20 (20%), tax = 20, gross = 120
            $net = $amount;
            $tax = $amount * $rate;
            $gross = $amount + $tax;
        }

        return [
            'tax' => $this->round($tax),
            'net' => $this->round($net),
            'gross' => $this->round($gross),
        ];
    }

    /**
     * Calculate tax for a line item (product with quantity)
     *
     * @param float $unit_price Unit price of the product
     * @param int $quantity Quantity
     * @param bool $price_includes_tax Whether the unit price already includes tax
     * @param float|null $custom_rate Optional custom tax rate (as decimal)
     * @return array ['tax' => tax_amount, 'net' => net_amount, 'gross' => gross_amount, 'unit_net' => unit_net, 'unit_gross' => unit_gross]
     */
    public function calculate_line_item($unit_price, $quantity, $price_includes_tax = null, $custom_rate = null) {
        $rounding_mode = $this->get_rounding_mode();

        if ($rounding_mode === 'line') {
            // Calculate per unit first, then multiply by quantity
            $unit_calc = $this->calculate($unit_price, $price_includes_tax, $custom_rate);

            return [
                'tax' => $this->round($unit_calc['tax'] * $quantity),
                'net' => $this->round($unit_calc['net'] * $quantity),
                'gross' => $this->round($unit_calc['gross'] * $quantity),
                'unit_net' => $unit_calc['net'],
                'unit_gross' => $unit_calc['gross'],
                'unit_tax' => $unit_calc['tax'],
            ];
        } else {
            // Calculate on line total
            $line_total = $unit_price * $quantity;
            $line_calc = $this->calculate($line_total, $price_includes_tax, $custom_rate);

            return [
                'tax' => $line_calc['tax'],
                'net' => $line_calc['net'],
                'gross' => $line_calc['gross'],
                'unit_net' => $this->round($line_calc['net'] / $quantity),
                'unit_gross' => $this->round($line_calc['gross'] / $quantity),
                'unit_tax' => $this->round($line_calc['tax'] / $quantity),
            ];
        }
    }

    /**
     * Calculate tax for cart items
     *
     * @param array $cart_items Array of cart items with 'price' and 'quantity' keys
     * @return array ['items' => calculated_items, 'subtotal_net' => net, 'subtotal_tax' => tax, 'subtotal_gross' => gross]
     */
    public function calculate_cart($cart_items) {
        $calculated_items = [];
        $subtotal_net = 0;
        $subtotal_tax = 0;
        $subtotal_gross = 0;

        foreach ($cart_items as $key => $item) {
            $price = isset($item['price']) ? floatval($item['price']) : 0;
            $quantity = isset($item['quantity']) ? intval($item['quantity']) : 1;

            $line_calc = $this->calculate_line_item($price, $quantity);

            $calculated_items[$key] = array_merge($item, [
                'line_net' => $line_calc['net'],
                'line_tax' => $line_calc['tax'],
                'line_gross' => $line_calc['gross'],
                'unit_net' => $line_calc['unit_net'],
                'unit_gross' => $line_calc['unit_gross'],
            ]);

            $subtotal_net += $line_calc['net'];
            $subtotal_tax += $line_calc['tax'];
            $subtotal_gross += $line_calc['gross'];
        }

        return [
            'items' => $calculated_items,
            'subtotal_net' => $this->round($subtotal_net),
            'subtotal_tax' => $this->round($subtotal_tax),
            'subtotal_gross' => $this->round($subtotal_gross),
        ];
    }

    /**
     * Round a value to 2 decimal places
     *
     * @param float $value
     * @return float
     */
    public function round($value) {
        return round($value, 2);
    }

    /**
     * Format price with currency
     *
     * @param float $amount
     * @return string
     */
    public function format_price($amount) {
        $currency_options = get_option('ggm_commerce_currency_options', []);
        $symbol = isset($currency_options['symbol']) ? $currency_options['symbol'] : '$';
        $position = isset($currency_options['position']) ? $currency_options['position'] : 'before';

        $formatted = number_format($amount, 2);

        switch ($position) {
            case 'after':
                return $formatted . $symbol;
            case 'before_space':
                return $symbol . ' ' . $formatted;
            case 'after_space':
                return $formatted . ' ' . $symbol;
            case 'before':
            default:
                return $symbol . $formatted;
        }
    }

    /**
     * Get the suffix display mode setting
     *
     * @return string 'auto', 'always', or 'never'
     */
    public function get_suffix_display_mode() {
        return isset($this->settings['show_suffix']) ? $this->settings['show_suffix'] : 'auto';
    }

    /**
     * Check if tax suffix should be displayed
     *
     * @param int|null $user_id WordPress user ID, or null for current user
     * @return bool
     */
    public function should_show_suffix($user_id = null) {
        // If tax is disabled, never show suffix
        if (!$this->is_enabled()) {
            return false;
        }

        // If admin has no VAT number, don't show suffix
        // (tax is invisible to customers, just part of the price)
        if (!$this->admin_has_vat_number()) {
            return false;
        }

        $mode = $this->get_suffix_display_mode();

        if ($mode === 'never') {
            return false;
        }

        if ($mode === 'always') {
            return true;
        }

        // Auto mode: show suffix based on effective display preference
        // This respects customer's choice if they've set a preference
        $effective_display = $this->get_effective_price_display($user_id);

        // Show suffix when prices are displayed excluding tax
        return $effective_display === 'excluding';
    }

    /**
     * Get the tax suffix text (e.g., "incl. VAT" or "excl. VAT")
     * Respects customer's price display preference
     *
     * @param bool|null $includes_tax Whether the price includes tax, or null to use effective display setting
     * @param int|null $user_id WordPress user ID, or null for current user
     * @return string
     */
    public function get_suffix_text($includes_tax = null, $user_id = null) {
        if ($includes_tax === null) {
            // Use the effective display preference (considers customer choice)
            $effective_display = $this->get_effective_price_display($user_id);
            $includes_tax = ($effective_display === 'including');
        }

        $label = $this->get_label();

        if ($includes_tax) {
            return sprintf(
                /* translators: %s: tax label like VAT or GST */
                __('incl. %s', 'ggm-commerce'),
                $label
            );
        } else {
            return sprintf(
                /* translators: %s: tax label like VAT or GST */
                __('excl. %s', 'ggm-commerce'),
                $label
            );
        }
    }

    /**
     * Format price with currency and optional tax suffix
     *
     * @param float $amount The price amount
     * @param bool $include_suffix Whether to include tax suffix (null = use setting)
     * @param int|null $user_id WordPress user ID, or null for current user
     * @return string Formatted price with optional suffix
     */
    public function format_price_with_suffix($amount, $include_suffix = null, $user_id = null) {
        $formatted_price = $this->format_price($amount);

        // Determine if we should show suffix
        $show_suffix = ($include_suffix !== null) ? $include_suffix : $this->should_show_suffix($user_id);

        if (!$show_suffix) {
            return $formatted_price;
        }

        $suffix = $this->get_suffix_text(null, $user_id);
        return $formatted_price . ' <span class="ggmc-tax-suffix">' . esc_html($suffix) . '</span>';
    }

    /**
     * Get just the suffix HTML for use in table headers etc.
     *
     * @param int|null $user_id WordPress user ID, or null for current user
     * @return string HTML suffix or empty string
     */
    public function get_suffix_html($user_id = null) {
        if (!$this->should_show_suffix($user_id)) {
            return '';
        }

        return ' <span class="ggmc-tax-suffix">(' . esc_html($this->get_suffix_text(null, $user_id)) . ')</span>';
    }

    /**
     * Get display price (net or gross depending on settings)
     *
     * @param float $price The base price (as stored in database)
     * @return float The price to display
     */
    public function get_display_price($price) {
        if (!$this->is_enabled()) {
            return $price;
        }

        // If prices include tax, they're already correct for display
        // If prices exclude tax, we might want to show gross depending on display settings
        // For now, return as-is since display mode determines how prices are entered
        return $price;
    }

    /**
     * Get customer's price display preference
     * Returns 'default', 'excluding', or 'including'
     *
     * @param int|null $user_id WordPress user ID, or null for current user
     * @return string
     */
    public function get_customer_price_display_preference($user_id = null) {
        if (!$user_id) {
            $user_id = get_current_user_id();
        }

        if (!$user_id) {
            return 'default';
        }

        $preference = get_user_meta($user_id, 'ggmc_price_display_preference', true);

        if (!in_array($preference, ['default', 'excluding', 'including'], true)) {
            return 'default';
        }

        return $preference;
    }

    /**
     * Determine how to display prices for current customer
     * Takes into account customer preference and store settings
     *
     * @param int|null $user_id WordPress user ID, or null for current user
     * @return string 'including' or 'excluding'
     */
    public function get_effective_price_display($user_id = null) {
        $preference = $this->get_customer_price_display_preference($user_id);

        if ($preference === 'default') {
            // Use store setting
            return $this->prices_include_tax() ? 'including' : 'excluding';
        }

        return $preference;
    }

    /**
     * Check if prices should be displayed including tax for current customer
     *
     * @param int|null $user_id WordPress user ID, or null for current user
     * @return bool
     */
    public function should_display_prices_with_tax($user_id = null) {
        return $this->get_effective_price_display($user_id) === 'including';
    }

    /**
     * Calculate display price based on customer preference
     * Converts price to either including or excluding tax based on preference
     *
     * @param float $price The base price (as stored in database)
     * @param int|null $user_id WordPress user ID, or null for current user
     * @return array ['price' => display_price, 'tax_suffix' => string]
     */
    public function calculate_display_price($price, $user_id = null) {
        if (!$this->is_enabled()) {
            return [
                'price' => $price,
                'tax_suffix' => '',
            ];
        }

        $stored_includes_tax = $this->prices_include_tax();
        $display_with_tax = $this->should_display_prices_with_tax($user_id);
        $rate = $this->get_rate();
        $label = $this->get_label();

        if ($stored_includes_tax === $display_with_tax) {
            // No conversion needed
            $suffix = $display_with_tax
                ? sprintf(
                    /* translators: %s: tax label like VAT or GST */
                    __('incl. %s', 'ggm-commerce'),
                    $label
                )
                : sprintf(
                    /* translators: %s: tax label like VAT or GST */
                    __('excl. %s', 'ggm-commerce'),
                    $label
                );

            return [
                'price' => $price,
                'tax_suffix' => $suffix,
            ];
        }

        if ($stored_includes_tax && !$display_with_tax) {
            // Stored price includes tax, but display without tax
            $net = $price / (1 + $rate);
            return [
                'price' => $this->round($net),
                'tax_suffix' => sprintf(
                    /* translators: %s: tax label like VAT or GST */
                    __('excl. %s', 'ggm-commerce'),
                    $label
                ),
            ];
        } else {
            // Stored price excludes tax, but display with tax
            $gross = $price * (1 + $rate);
            return [
                'price' => $this->round($gross),
                'tax_suffix' => sprintf(
                    /* translators: %s: tax label like VAT or GST */
                    __('incl. %s', 'ggm-commerce'),
                    $label
                ),
            ];
        }
    }

    /**
     * Get all tax settings as array
     */
    public function get_settings() {
        return [
            'enabled' => $this->is_enabled(),
            'display_mode' => $this->get_display_mode(),
            'prices_include_tax' => $this->prices_include_tax(),
            'rate' => $this->get_rate(),
            'rate_percentage' => $this->get_rate_percentage(),
            'label' => $this->get_label(),
            'rounding_mode' => $this->get_rounding_mode(),
            'b2b_enabled' => $this->is_b2b_enabled(),
            'b2b_validation_required' => $this->is_b2b_validation_required(),
        ];
    }

    /**
     * Check if a customer is tax exempt
     * Priority: 1) Customer account approval, 2) Session-based exemption, 3) User meta fallback
     *
     * @param int|null $customer_id GGM Commerce customer ID or null for current session/user
     * @return bool
     */
    public function is_customer_tax_exempt($customer_id = null) {
        if (!$this->is_b2b_enabled()) {
            return false;
        }

        // If specific customer ID provided, check customer table
        if ($customer_id) {
            return $this->get_customer_tax_exempt_status($customer_id);
        }

        // Check current logged-in user first (account-based exemption takes priority)
        $user_id = get_current_user_id();
        if ($user_id) {
            // Look up customer by user_id
            global $wpdb;
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $customer = $wpdb->get_row($wpdb->prepare(
                "SELECT is_tax_exempt, tax_exempt_status FROM {$wpdb->prefix}ggmc_customers WHERE user_id = %d",
                $user_id
            ));

            if ($customer) {
                // Check if approved
                if ($customer->is_tax_exempt && $customer->tax_exempt_status === 'approved') {
                    return true;
                }
                // Auto-approve if validation not required and status is pending
                if ($customer->is_tax_exempt && !$this->is_b2b_validation_required() && $customer->tax_exempt_status === 'pending') {
                    return true;
                }
            }

            // Fallback to user meta for backwards compatibility
            $is_exempt = get_user_meta($user_id, 'ggmc_tax_exempt', true);
            if (!empty($is_exempt)) {
                return true;
            }
        }

        // For guests or as additional exemption, check session-based exemption (checkout flow)
        if ($this->get_session_tax_exempt()) {
            return true;
        }

        return false;
    }

    /**
     * Get customer tax exempt status from database
     *
     * @param int $customer_id GGM Commerce customer ID
     * @return bool
     */
    private function get_customer_tax_exempt_status($customer_id) {
        global $wpdb;
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $customer = $wpdb->get_row($wpdb->prepare(
            "SELECT is_tax_exempt, tax_exempt_status FROM {$wpdb->prefix}ggmc_customers WHERE id = %d",
            $customer_id
        ));

        if (!$customer) {
            return false;
        }

        if ($customer->is_tax_exempt && $customer->tax_exempt_status === 'approved') {
            return true;
        }
        if ($customer->is_tax_exempt && !$this->is_b2b_validation_required() && $customer->tax_exempt_status === 'pending') {
            return true;
        }

        return false;
    }

    /**
     * Get customer tax ID from customer table
     *
     * @param int|null $customer_id GGM Commerce customer ID or null for current user
     * @return string|null
     */
    public function get_customer_tax_id($customer_id = null) {
        global $wpdb;

        if ($customer_id) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $tax_id = $wpdb->get_var($wpdb->prepare(
                "SELECT tax_id FROM {$wpdb->prefix}ggmc_customers WHERE id = %d",
                $customer_id
            ));
            return $tax_id ?: null;
        }

        $user_id = get_current_user_id();
        if (!$user_id) {
            return null;
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $tax_id = $wpdb->get_var($wpdb->prepare(
            "SELECT tax_id FROM {$wpdb->prefix}ggmc_customers WHERE user_id = %d",
            $user_id
        ));

        // Fallback to user meta
        if (!$tax_id) {
            $tax_id = get_user_meta($user_id, 'ggmc_tax_id', true);
        }

        return $tax_id ?: null;
    }

    /**
     * Set customer tax exempt status in customer table
     *
     * @param int $customer_id GGM Commerce customer ID
     * @param bool $is_exempt
     * @param string $tax_id Optional tax ID
     * @param string $status Optional status: 'none', 'pending', 'approved', 'rejected'
     * @return bool Success
     */
    public function set_customer_tax_exempt($customer_id, $is_exempt, $tax_id = '', $status = null) {
        global $wpdb;

        $data = [
            'is_tax_exempt' => $is_exempt ? 1 : 0,
        ];

        if ($tax_id !== '') {
            $data['tax_id'] = sanitize_text_field($tax_id);
        }

        if ($status !== null) {
            $data['tax_exempt_status'] = sanitize_text_field($status);
        } else if ($is_exempt) {
            // Set default status based on validation requirement
            $data['tax_exempt_status'] = $this->is_b2b_validation_required() ? 'pending' : 'approved';
        } else {
            $data['tax_exempt_status'] = 'none';
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $result = $wpdb->update(
            "{$wpdb->prefix}ggmc_customers",
            $data,
            ['id' => $customer_id]
        );

        return $result !== false;
    }

    /**
     * Set session-based tax exemption (for checkout before customer record exists)
     *
     * @param bool $exempt
     * @param string $tax_id
     */
    public function set_session_tax_exempt($exempt, $tax_id = '') {
        if (session_status() === PHP_SESSION_NONE && !headers_sent()) {
            session_start();
        }

        if ($exempt) {
            $_SESSION['ggmc_b2b_tax_exempt'] = true;
            $_SESSION['ggmc_b2b_tax_id'] = sanitize_text_field($tax_id);
        } else {
            unset($_SESSION['ggmc_b2b_tax_exempt']);
            unset($_SESSION['ggmc_b2b_tax_id']);
        }
    }

    /**
     * Get session-based tax exemption status
     *
     * @return bool
     */
    public function get_session_tax_exempt() {
        if (session_status() === PHP_SESSION_NONE && !headers_sent()) {
            session_start();
        }

        return !empty($_SESSION['ggmc_b2b_tax_exempt']);
    }

    /**
     * Get session-based tax ID
     *
     * @return string|null
     */
    public function get_session_tax_id() {
        if (session_status() === PHP_SESSION_NONE && !headers_sent()) {
            session_start();
        }

        return isset($_SESSION['ggmc_b2b_tax_id']) ? $_SESSION['ggmc_b2b_tax_id'] : null;
    }

    /**
     * Clear session-based tax exemption
     */
    public function clear_session_tax_exempt() {
        if (session_status() === PHP_SESSION_NONE && !headers_sent()) {
            session_start();
        }

        unset($_SESSION['ggmc_b2b_tax_exempt']);
        unset($_SESSION['ggmc_b2b_tax_id']);
    }

    /**
     * Calculate tax considering customer exemption
     *
     * @param float $amount
     * @param bool $price_includes_tax
     * @param int|null $customer_id
     * @return array
     */
    public function calculate_with_exemption($amount, $price_includes_tax = null, $customer_id = null) {
        // Check if customer is tax exempt
        if ($this->is_customer_tax_exempt($customer_id)) {
            // Return zero tax
            $includes_tax = $price_includes_tax !== null ? $price_includes_tax : $this->prices_include_tax();

            if ($includes_tax) {
                // Extract tax from price to get net amount
                $rate = $this->get_rate();
                $net = $amount / (1 + $rate);
                return [
                    'tax' => 0,
                    'net' => $this->round($net),
                    'gross' => $this->round($net), // Gross equals net for exempt customers
                    'exempt' => true,
                ];
            } else {
                return [
                    'tax' => 0,
                    'net' => $amount,
                    'gross' => $amount,
                    'exempt' => true,
                ];
            }
        }

        // Normal tax calculation
        $result = $this->calculate($amount, $price_includes_tax);
        $result['exempt'] = false;
        return $result;
    }

    // =========================================================================
    // TAX RATES CRUD METHODS
    // =========================================================================

    /**
     * Check if admin business has a VAT/Tax number registered
     * Required for ex-VAT pricing and B2B features
     *
     * @return bool
     */
    public function admin_has_vat_number() {
        $business_options = get_option('ggm_commerce_business_options', []);
        return !empty($business_options['tax_id']);
    }

    /**
     * Get admin's VAT/Tax number
     *
     * @return string|null
     */
    public function get_admin_vat_number() {
        $business_options = get_option('ggm_commerce_business_options', []);
        return !empty($business_options['tax_id']) ? $business_options['tax_id'] : null;
    }

    /**
     * Get all tax rates
     *
     * @param array $args Optional query arguments: status, orderby, order
     * @return array Array of tax rate objects
     */
    public function get_tax_rates($args = []) {
        global $wpdb;

        $defaults = [
            'status' => 'active',
            'orderby' => 'display_order',
            'order' => 'ASC',
        ];
        $args = wp_parse_args($args, $defaults);

        $where = [];
        $params = [];

        if ($args['status'] !== 'all') {
            $where[] = 'status = %s';
            $params[] = $args['status'];
        }

        $where_clause = !empty($where) ? 'WHERE ' . implode(' AND ', $where) : '';
        $order_clause = sprintf('ORDER BY %s %s', esc_sql($args['orderby']), esc_sql($args['order']));

        // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $where_clause and $order_clause are safely built above
        $sql = "SELECT * FROM {$wpdb->prefix}ggmc_tax_rates {$where_clause} {$order_clause}";

        if (!empty($params)) {
            // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- $sql uses safe interpolation
            $sql = $wpdb->prepare($sql, $params);
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.NotPrepared -- $sql prepared above if needed
        return $wpdb->get_results($sql);
    }

    /**
     * Get a single tax rate by ID or slug
     *
     * @param int|string $identifier Tax rate ID or slug
     * @param bool $by_slug If true, lookup by slug instead of ID
     * @return object|null Tax rate object or null
     */
    public function get_tax_rate($identifier, $by_slug = false) {
        $cache_key = ($by_slug ? 'slug:' : 'id:') . $identifier;
        if (isset(self::$tax_rate_cache[$cache_key])) {
            return self::$tax_rate_cache[$cache_key];
        }

        global $wpdb;

        if ($by_slug) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $result = $wpdb->get_row($wpdb->prepare(
                "SELECT * FROM {$wpdb->prefix}ggmc_tax_rates WHERE slug = %s",
                $identifier
            ));
        } else {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $result = $wpdb->get_row($wpdb->prepare(
                "SELECT * FROM {$wpdb->prefix}ggmc_tax_rates WHERE id = %d",
                $identifier
            ));
        }

        self::$tax_rate_cache[$cache_key] = $result;
        return $result;
    }

    /**
     * Get the default tax rate
     *
     * @return object|null Tax rate object or null
     */
    public function get_default_tax_rate() {
        if (self::$default_tax_rate_loaded) {
            return self::$default_tax_rate_cache;
        }

        global $wpdb;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $rate = $wpdb->get_row(
            "SELECT * FROM {$wpdb->prefix}ggmc_tax_rates WHERE is_default = 1 AND status = 'active' LIMIT 1"
        );

        // Fallback to first active rate if no default is set
        if (!$rate) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $rate = $wpdb->get_row(
                "SELECT * FROM {$wpdb->prefix}ggmc_tax_rates WHERE status = 'active' ORDER BY display_order ASC LIMIT 1"
            );
        }

        self::$default_tax_rate_cache = $rate;
        self::$default_tax_rate_loaded = true;
        return $rate;
    }

    /**
     * Get tax rate for a product
     * Returns the product's assigned tax rate, or the default rate if not set
     *
     * @param int $product_id Product ID
     * @return object|null Tax rate object
     */
    public function get_product_tax_rate($product_id) {
        if (isset(self::$product_tax_rate_cache[$product_id])) {
            return self::$product_tax_rate_cache[$product_id];
        }

        global $wpdb;

        // Get product's tax_rate_id
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $tax_rate_id = $wpdb->get_var($wpdb->prepare(
            "SELECT tax_rate_id FROM {$wpdb->prefix}ggmc_products WHERE id = %d",
            $product_id
        ));

        if ($tax_rate_id) {
            $rate = $this->get_tax_rate($tax_rate_id);
            if ($rate) {
                self::$product_tax_rate_cache[$product_id] = $rate;
                return $rate;
            }
        }

        // Fall back to default rate
        $rate = $this->get_default_tax_rate();
        self::$product_tax_rate_cache[$product_id] = $rate;
        return $rate;
    }

    /**
     * Get tax rate percentage for a product (as decimal)
     *
     * @param int $product_id Product ID
     * @return float Tax rate as decimal (e.g., 0.20 for 20%)
     */
    public function get_product_tax_rate_decimal($product_id) {
        $tax_rate = $this->get_product_tax_rate($product_id);
        if ($tax_rate) {
            return floatval($tax_rate->rate) / 100;
        }
        return 0;
    }

    /**
     * Get all tax rates
     *
     * @param bool $active_only Only return active tax rates
     * @return array Array of tax rate objects
     */
    public function get_all_tax_rates($active_only = false) {
        global $wpdb;

        $where = $active_only ? "WHERE status = 'active'" : '';

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $where is safely built above
        return $wpdb->get_results(
            "SELECT * FROM {$wpdb->prefix}ggmc_tax_rates {$where} ORDER BY display_order ASC, name ASC"
        );
    }

    /**
     * Create a new tax rate
     *
     * @param array $data Tax rate data: name, rate, slug (optional), description (optional)
     * @return int|false New tax rate ID or false on failure
     */
    public function create_tax_rate($data) {
        global $wpdb;

        // Validate required fields
        if (empty($data['name']) || !isset($data['rate'])) {
            return false;
        }

        // Generate slug if not provided
        $slug = !empty($data['slug']) ? sanitize_title($data['slug']) : sanitize_title($data['name']);

        // Ensure unique slug
        $original_slug = $slug;
        $counter = 1;
        while ($this->get_tax_rate($slug, true)) {
            $slug = $original_slug . '-' . $counter;
            $counter++;
        }

        // Get next display order
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $max_order = $wpdb->get_var("SELECT MAX(display_order) FROM {$wpdb->prefix}ggmc_tax_rates");
        $display_order = $max_order ? $max_order + 1 : 1;

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $result = $wpdb->insert(
            "{$wpdb->prefix}ggmc_tax_rates",
            [
                'name' => sanitize_text_field($data['name']),
                'slug' => $slug,
                'rate' => floatval($data['rate']),
                'description' => isset($data['description']) ? sanitize_textarea_field($data['description']) : '',
                'is_default' => !empty($data['is_default']) ? 1 : 0,
                'status' => isset($data['status']) ? sanitize_text_field($data['status']) : 'active',
                'display_order' => $display_order,
            ],
            ['%s', '%s', '%f', '%s', '%d', '%s', '%d']
        );

        if ($result === false) {
            return false;
        }

        $new_id = $wpdb->insert_id;

        // If this is set as default, unset other defaults
        if (!empty($data['is_default'])) {
            $this->set_default_tax_rate($new_id);
        }

        return $new_id;
    }

    /**
     * Update an existing tax rate
     *
     * @param int $rate_id Tax rate ID
     * @param array $data Data to update
     * @return bool Success
     */
    public function update_tax_rate($rate_id, $data) {
        global $wpdb;

        $existing = $this->get_tax_rate($rate_id);
        if (!$existing) {
            return false;
        }

        $update_data = [];
        $format = [];

        if (isset($data['name'])) {
            $update_data['name'] = sanitize_text_field($data['name']);
            $format[] = '%s';
        }

        if (isset($data['rate'])) {
            $update_data['rate'] = floatval($data['rate']);
            $format[] = '%f';
        }

        if (isset($data['description'])) {
            $update_data['description'] = sanitize_textarea_field($data['description']);
            $format[] = '%s';
        }

        if (isset($data['status'])) {
            $update_data['status'] = sanitize_text_field($data['status']);
            $format[] = '%s';
        }

        if (isset($data['display_order'])) {
            $update_data['display_order'] = intval($data['display_order']);
            $format[] = '%d';
        }

        if (isset($data['slug'])) {
            $new_slug = sanitize_title($data['slug']);
            // Check if slug is unique (excluding current rate)
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $existing_with_slug = $wpdb->get_var($wpdb->prepare(
                "SELECT id FROM {$wpdb->prefix}ggmc_tax_rates WHERE slug = %s AND id != %d",
                $new_slug,
                $rate_id
            ));
            if (!$existing_with_slug) {
                $update_data['slug'] = $new_slug;
                $format[] = '%s';
            }
        }

        if (empty($update_data)) {
            return true; // Nothing to update
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $result = $wpdb->update(
            "{$wpdb->prefix}ggmc_tax_rates",
            $update_data,
            ['id' => $rate_id],
            $format,
            ['%d']
        );

        // Handle default flag separately
        if (isset($data['is_default']) && $data['is_default']) {
            $this->set_default_tax_rate($rate_id);
        }

        return $result !== false;
    }

    /**
     * Set a tax rate as the default (and unset others)
     *
     * @param int $rate_id Tax rate ID to set as default
     * @return bool Success
     */
    public function set_default_tax_rate($rate_id) {
        global $wpdb;

        // Unset all defaults first
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->update(
            "{$wpdb->prefix}ggmc_tax_rates",
            ['is_default' => 0],
            ['is_default' => 1],
            ['%d'],
            ['%d']
        );

        // Set the new default
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        return $wpdb->update(
            "{$wpdb->prefix}ggmc_tax_rates",
            ['is_default' => 1],
            ['id' => $rate_id],
            ['%d'],
            ['%d']
        ) !== false;
    }

    /**
     * Delete a tax rate
     * Products using this rate will fall back to the default rate
     *
     * @param int $rate_id Tax rate ID
     * @return bool Success
     */
    public function delete_tax_rate($rate_id) {
        global $wpdb;

        $existing = $this->get_tax_rate($rate_id);
        if (!$existing) {
            return false;
        }

        // Don't allow deleting the default rate if it's the only active one
        if ($existing->is_default) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $other_active = $wpdb->get_var($wpdb->prepare(
                "SELECT COUNT(*) FROM {$wpdb->prefix}ggmc_tax_rates WHERE status = 'active' AND id != %d",
                $rate_id
            ));
            if ($other_active == 0) {
                return false; // Can't delete the only active rate
            }
        }

        // Clear tax_rate_id from products using this rate
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $wpdb->update(
            "{$wpdb->prefix}ggmc_products",
            ['tax_rate_id' => null],
            ['tax_rate_id' => $rate_id],
            ['%s'],
            ['%d']
        );

        // Delete the rate
        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $result = $wpdb->delete(
            "{$wpdb->prefix}ggmc_tax_rates",
            ['id' => $rate_id],
            ['%d']
        );

        // If we deleted the default, set a new default
        if ($result && $existing->is_default) {
            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $first_active = $wpdb->get_var(
                "SELECT id FROM {$wpdb->prefix}ggmc_tax_rates WHERE status = 'active' ORDER BY display_order ASC LIMIT 1"
            );
            if ($first_active) {
                $this->set_default_tax_rate($first_active);
            }
        }

        return $result !== false;
    }

    /**
     * Calculate tax for a product using its assigned tax rate
     * Prices are always stored inclusive of tax
     *
     * @param float $price The product price (always inclusive of tax)
     * @param int $product_id Product ID to get tax rate from
     * @param bool $display_excluding Whether to return the excluding-tax price
     * @return array ['gross' => price_with_tax, 'net' => price_without_tax, 'tax' => tax_amount, 'rate' => rate_object]
     */
    public function calculate_product_tax($price, $product_id, $display_excluding = false) {
        if (!$this->is_enabled()) {
            return [
                'gross' => $price,
                'net' => $price,
                'tax' => 0,
                'rate' => null,
            ];
        }

        $tax_rate = $this->get_product_tax_rate($product_id);
        $rate_decimal = $tax_rate ? floatval($tax_rate->rate) / 100 : 0;

        if ($rate_decimal <= 0) {
            return [
                'gross' => $price,
                'net' => $price,
                'tax' => 0,
                'rate' => $tax_rate,
            ];
        }

        // Price is always stored inclusive - extract the net
        $net = $price / (1 + $rate_decimal);
        $tax = $price - $net;

        return [
            'gross' => $this->round($price),
            'net' => $this->round($net),
            'tax' => $this->round($tax),
            'rate' => $tax_rate,
        ];
    }

    /**
     * Get the display price for a product considering customer preference
     *
     * @param float $price The stored price (always inclusive of tax)
     * @param int $product_id Product ID
     * @param int|null $user_id User ID or null for current user
     * @return array ['price' => display_price, 'suffix' => tax_suffix_text]
     */
    public function get_product_display_price($price, $product_id, $user_id = null) {
        if (!$this->is_enabled() || !$this->admin_has_vat_number()) {
            // Tax not enabled or admin has no VAT number - just return the price
            return [
                'price' => $price,
                'suffix' => '',
            ];
        }

        $tax_calc = $this->calculate_product_tax($price, $product_id);
        $display_with_tax = $this->should_display_prices_with_tax($user_id);
        $label = $this->get_label();

        if ($display_with_tax) {
            return [
                'price' => $tax_calc['gross'],
                'suffix' => $this->should_show_suffix($user_id) ? sprintf(
                    /* translators: %s: tax label (e.g., "VAT", "GST") */
                    __('incl. %s', 'ggm-commerce'),
                    $label
                ) : '',
            ];
        } else {
            return [
                'price' => $tax_calc['net'],
                'suffix' => $this->should_show_suffix($user_id) ? sprintf(
                    /* translators: %s: tax label (e.g., "VAT", "GST") */
                    __('excl. %s', 'ggm-commerce'),
                    $label
                ) : '',
            ];
        }
    }
}

/**
 * Helper function to get Tax instance
 */
function ggm_commerce_tax() {
    return GGM_Commerce_Tax::get_instance();
}
