<?php

/**
 * Member Discount Hooks
 *
 * Integrates member-only discounts with all WooCommerce pricing surfaces.
 *
 * Pricing flow overview (order matters):
 * ┌─────────────────────────────────────────────────────┐
 * │  1. Catalog / Product page  (price display)         │
 * │     • woocommerce_product_get_price                 │
 * │     • woocommerce_product_get_sale_price             │
 * │     • woocommerce_get_price_html                    │
 * │     • woocommerce_variation_prices_price             │
 * │     • woocommerce_variation_prices_sale_price        │
 * │                                                     │
 * │  2. Cart  (line-item pricing / cart fees)           │
 * │     • Same product price hooks fire during totals    │
 * │     • woocommerce_cart_calculate_fees                │
 * │                                                     │
 * │  3. Checkout (order total)                          │
 * │     • Same product price hooks fire during totals    │
 * │                                                     │
 * │  4. Coupon coexistence                              │
 * │     • arraysubs_member_discount_stack_with_coupons   │
 * └─────────────────────────────────────────────────────┘
 *
 * All hooks are added at a late priority (100) so other plugins
 * (including subscription display helpers) have already set their values.
 *
 * @package ArraySubs\Features\MembersAccess\Services
 * @since 1.9.0
 */

namespace ArraySubs\Features\MembersAccess\Services;

defined('ABSPATH') || exit;

use ArraySubs\Supports\Config;

/**
 * DiscountHooks class
 */
class DiscountHooks
{
    /**
     * Enable auto-loading
     *
     * @var bool
     */
    public static $loadable = true;

    /**
     * Constructor — registers all WooCommerce price hooks.
     */
    public function __construct()
    {
        // Only run on the frontend — not in admin (except AJAX), REST, or WP-CLI
        if ($this->isAdminContext()) {
            return;
        }

        // Only run when Members Access is enabled
        if (!(bool) arraysubs_get_setting('members_access.enabled', true)) {
            return;
        }

        // ── Simple & grouped product prices ──
        add_filter('woocommerce_product_get_price', [$this, 'filterPrice'], 100, 2);
        add_filter('woocommerce_product_get_sale_price', [$this, 'filterSalePrice'], 100, 2);

        // ── Variable product variation prices ──
        add_filter('woocommerce_product_variation_get_price', [$this, 'filterPrice'], 100, 2);
        add_filter('woocommerce_product_variation_get_sale_price', [$this, 'filterSalePrice'], 100, 2);

        // ── Variation price range (from–to display) ──
        add_filter('woocommerce_variation_prices_price', [$this, 'filterVariationPricesPrice'], 100, 3);
        add_filter('woocommerce_variation_prices_sale_price', [$this, 'filterVariationPricesSalePrice'], 100, 3);

        // ── Variation price hash — ensures cached ranges include member state ──
        add_filter('woocommerce_get_variation_prices_hash', [$this, 'addVariationPriceHash'], 100, 3);

        // ── Price HTML (strikethrough / "Member Price" badge) ──
        add_filter('woocommerce_get_price_html', [$this, 'filterPriceHtml'], 100, 2);

        // ── Coupon coexistence ──
        add_filter('woocommerce_coupon_is_valid', [$this, 'maybeLimitCoupons'], 100, 3);

        // ── Per-cart fixed discounts ──
        add_action('woocommerce_cart_calculate_fees', [$this, 'applyCartDiscount'], 30, 1);

        // ── Cache invalidation on login / logout ──
        add_action('wp_login', [DiscountEngine::class, 'clearCache']);
        add_action('wp_logout', [DiscountEngine::class, 'clearCache']);
        add_action('woocommerce_checkout_order_processed', [DiscountEngine::class, 'clearCache']);

        // ── Frontend badge styles ──
        add_action('wp_enqueue_scripts', [$this, 'enqueueFrontendStyles']);
    }

    // ──────────────────────────────────────────────
    // Price filters.
    // ──────────────────────────────────────────────

    /**
     * Filter the product price for simple, grouped, and variation products.
     *
     * @param string      $price Current price.
     * @param \WC_Product $product Product object.
     * @return string
     */
    public function filterPrice($price, $product)
    {
        $discounted = DiscountEngine::getDiscountedPrice($product, get_current_user_id());
        if ($discounted !== false) {
            return (string) $discounted;
        }

        return $price;
    }

    /**
     * Filter the sale price — set it to the member discount so WooCommerce
     * automatically shows the strikethrough regular price.
     *
     * @param string      $sale_price Current sale price.
     * @param \WC_Product $product    Product object.
     * @return string
     */
    public function filterSalePrice($sale_price, $product)
    {
        $discounted = DiscountEngine::getDiscountedPrice($product, get_current_user_id());
        if ($discounted !== false) {
            return (string) $discounted;
        }

        return $sale_price;
    }

    /**
     * Filter variation prices used for the "from–to" range display.
     *
     * @param string                $price     The price.
     * @param \WC_Product_Variation $variation Variation product.
     * @param \WC_Product_Variable  $parent    Parent variable product.
     * @return string
     */
    public function filterVariationPricesPrice($price, $variation, $parent)
    {
        $discounted = DiscountEngine::getDiscountedPrice($variation, get_current_user_id());
        if ($discounted !== false) {
            return (string) $discounted;
        }

        return $price;
    }

    /**
     * Filter variation sale prices used for the "from–to" range display.
     *
     * @param string                $price     The sale price.
     * @param \WC_Product_Variation $variation Variation product.
     * @param \WC_Product_Variable  $parent    Parent variable product.
     * @return string
     */
    public function filterVariationPricesSalePrice($price, $variation, $parent)
    {
        $discounted = DiscountEngine::getDiscountedPrice($variation, get_current_user_id());
        if ($discounted !== false) {
            return (string) $discounted;
        }

        return $price;
    }

    /**
     * Add user-specific key to the variation price hash.
     *
     * WooCommerce caches variation price ranges per hash. Without a
     * user-specific component, logged-in members see stale (non-discounted)
     * prices from the transient cache.
     *
     * @param array                $hash        Hash components.
     * @param \WC_Product_Variable $product     Variable product.
     * @param bool                 $for_display Whether this is for display.
     * @return array
     */
    public function addVariationPriceHash($hash, $product, $for_display)
    {
        $user_id = get_current_user_id();
        if ($user_id > 0 && DiscountEngine::userHasDiscounts($user_id)) {
            $hash[] = 'arraysubs_member_' . $user_id;
        }

        return $hash;
    }

    // ──────────────────────────────────────────────
    // Price HTML.
    // ──────────────────────────────────────────────

    /**
     * Modify price HTML to show original price + member price with a badge.
     *
     * Only modifies simple/external products here; variations handle their
     * own display via the price/sale_price filters above.
     *
     * @param string      $html    Current price HTML.
     * @param \WC_Product $product Product object.
     * @return string
     */
    public function filterPriceHtml($html, $product)
    {
        // Skip grouped and variable products — they show a range
        // that is already modified via variation price filters
        if ($product->is_type('variable') || $product->is_type('grouped')) {
            return $html;
        }

        $user_id    = get_current_user_id();
        $discounted = DiscountEngine::getDiscountedPrice($product, $user_id);

        if ($discounted === false) {
            return $html;
        }

        $regular_price = (float) $product->get_regular_price('edit');
        if ($regular_price <= 0 || $discounted >= $regular_price) {
            return $html;
        }

        /**
         * Action fired after a discount is visually applied to a product price.
         *
         * Useful for analytics, logging, or cache-busting side effects.
         *
         * @since 1.9.0
         *
         * @param float       $discounted    The discounted price.
         * @param float       $regular_price The regular price before discount.
         * @param \WC_Product $product       The product.
         * @param int         $user_id       The current user.
         */
        do_action('arraysubs_member_discount_applied', $discounted, $regular_price, $product, $user_id);

        $badge_label = esc_html__('Member Price', 'arraysubs');

        /**
         * Filter the "Member Price" badge label shown next to discounted prices.
         *
         * @since 1.9.0
         *
         * @param string      $badge_label The badge label text.
         * @param \WC_Product $product     The product.
         * @param float       $discounted  The discounted price.
         */
        $badge_label = apply_filters('arraysubs_member_discount_badge_label', $badge_label, $product, $discounted);

        $savings_pct = round((($regular_price - $discounted) / $regular_price) * 100);

        $price_html = '<del aria-hidden="true">' . wc_price($regular_price) . '</del> '
            . '<ins>' . wc_price($discounted) . '</ins>';

        $badge_html = '<span class="arraysubs-member-discount-badge">'
            . $badge_label
            . '</span>';

        $savings_html = '';
        if ($savings_pct > 0) {
            $savings_html = '<span class="arraysubs-member-discount-savings">'
                . sprintf(
                    /* translators: %d: discount percentage */
                    esc_html__('Save %d%%', 'arraysubs'),
                    $savings_pct
                )
                . '</span>';
        }

        return '<span class="arraysubs-member-discount-price">'
            . $price_html . ' ' . $badge_html . ' ' . $savings_html
            . '</span>';
    }

    // ──────────────────────────────────────────────
    // Coupon coexistence.
    // ──────────────────────────────────────────────

    /**
     * Apply the best per-cart member discount as a negative fee.
     *
     * @param \WC_Cart $cart Cart object.
     * @return void
     */
    public function applyCartDiscount($cart): void
    {
        if (!($cart instanceof \WC_Cart) || $cart->is_empty()) {
            return;
        }

        $user_id = get_current_user_id();
        if ($user_id <= 0) {
            return;
        }

        $discount = DiscountEngine::getCartDiscountData($cart, $user_id);
        if (empty($discount['amount']) || $discount['amount'] <= 0) {
            return;
        }

        $label = __('Member discount', 'arraysubs');

        /**
         * Filter the cart fee label for per-cart member discounts.
         *
         * @since 1.9.0
         *
         * @param string $label The fee label.
         * @param array  $rule  The matched discount rule.
         * @param int    $user_id Current user ID.
         */
        $label = apply_filters(
            'arraysubs_member_cart_discount_label',
            $label,
            $discount['rule'],
            $user_id
        );

        $cart->add_fee($label, -1 * (float) $discount['amount'], false);
    }

    /**
     * Optionally prevent coupons when a member discount is active.
     *
     * By default member discounts and coupons coexist. Merchants can change
     * this behaviour via the `arraysubs_member_discount_stack_with_coupons` filter.
     *
     * @param bool          $valid     Whether the coupon is valid.
     * @param \WC_Coupon    $coupon    Coupon object.
     * @param \WC_Discounts $discounts Discounts context.
     *
     * @throws \Exception When coupon stacking is disabled via filter.
     * @return bool
     */
    public function maybeLimitCoupons($valid, $coupon, $discounts)
    {
        if (!$valid) {
            return $valid;
        }

        $user_id = get_current_user_id();
        if ($user_id <= 0 || !DiscountEngine::userHasDiscounts($user_id)) {
            return $valid;
        }

        /**
         * Filter whether member discounts coexist with coupons.
         *
         * Return false to prevent coupon usage when a member discount is active.
         *
         * @since 1.9.0
         *
         * @param bool       $allow   Whether to allow coupons alongside member discounts.
         * @param \WC_Coupon $coupon  The coupon being validated.
         * @param int        $user_id The current user.
         */
        $allow_stack = apply_filters('arraysubs_member_discount_stack_with_coupons', true, $coupon, $user_id);

        if (!$allow_stack) {
            throw new \Exception(
                esc_html__('Coupons cannot be used alongside your member discount.', 'arraysubs')
            );
        }

        return $valid;
    }

    // ──────────────────────────────────────────────
    // Frontend assets.
    // ──────────────────────────────────────────────

    /**
     * Enqueue the member discount badge stylesheet on the frontend.
     *
     * Only loads when the current user has qualifying discount rules.
     *
     * @return void
     */
    public function enqueueFrontendStyles(): void
    {
        $user_id = get_current_user_id();

        if ($user_id <= 0 || !DiscountEngine::userHasDiscounts($user_id)) {
            return;
        }

        $asset_file = Config::get('plugin.public_path') . 'build/member-discount.asset.php';
        $asset      = file_exists($asset_file)
            ? require $asset_file
            : ['dependencies' => [], 'version' => Config::get('plugin.version')];

        wp_enqueue_style(
            'arraysubs-member-discount',
            Config::get('plugin.public_url') . 'build/member-discount.css',
            [],
            $asset['version']
        );
    }

    // ──────────────────────────────────────────────
    // Utilities.
    // ──────────────────────────────────────────────

    /**
     * Check if we're in an admin context that should skip discount hooks.
     *
     * Discounts should apply on:
     * - Frontend pages
     * - AJAX requests (cart, checkout fragments)
     * - REST API requests from storefront (block-based store)
     *
     * Discounts should NOT apply in:
     * - wp-admin pages (order editing, product editing)
     * - WP-CLI commands
     *
     * @return bool True if hooks should be skipped.
     */
    private function isAdminContext(): bool
    {
        // AJAX requests should get discounted prices (cart, mini-cart)
        if (wp_doing_ajax()) {
            return false;
        }

        // REST API requests should get discounted prices (Store API, block cart)
        if (defined('REST_REQUEST') && REST_REQUEST) {
            return false;
        }

        // WP-CLI — skip
        if (defined('WP_CLI') && WP_CLI) {
            return true;
        }

        // Admin (but not AJAX/REST) — skip
        return is_admin();
    }
}
