<?php

namespace CouponsPlus\App\Coupon;

use CouponsPlus\App\Coupon\Coupon;
use CouponsPlus\App\Coupon\CouponOffersTracker;
use CouponsPlus\App\Coupon\CouponRegistry;
use CouponsPlus\App\Coupon\Tree\Notifications\Notifications;
use CouponsPlus\App\Coupon\Tree\Factories\CouponFromClassicCouponFactory;
use CouponsPlus\App\Offers\OffersScheduler;
use CouponsPlus\Original\Cache\MemoryCache;
use CouponsPlus\Original\Collections\Collection;
use CouponsPlus\Original\Environment\Env;
use WC_Coupon;
use WC_Discounts;

use function WC;

#[\AllowDynamicProperties]
Class CouponsManager
{
    protected $cache;
    protected $couponFactory;
    protected bool $removingInvalidCoupons = false;

    //const MANAGE_HOOK = 'woocommerce_coupon_get_items_to_validate';
    const MANAGE_HOOK = 'woocommerce_coupon_get_items_to_validate';

    public static function isLegacy(): bool
    {
        static $isLegacy = null;

        if ($isLegacy === null) {
            $isLegacy = apply_filters(Env::getWithPrefix('is_legacy_installation'), false);
        }

        return $isLegacy;
    }
    public function __construct()
    {
        $this->cache = new MemoryCache;   
        $this->couponOffersTracker = CouponOffersTracker::getInstance();
        $this->offersScheduler = new OffersScheduler;
        $this->couponFactory = new CouponFromClassicCouponFactory($this->couponOffersTracker);
        //$this->couponURLManager = new CouponURLsManager;
    }
    
    public function registerEvents()
    {
        add_filter(static::MANAGE_HOOK, [$this, 'manage'], 10, 2);
        add_filter('woocommerce_cart_reset', [$this, 'onCartReset'], 10);
        add_action('woocommerce_before_calculate_totals', [$this, 'resetGlobalState'], 10);
        add_action('woocommerce_after_calculate_totals', [$this, 'autoApplyCoupons'], 10);
        add_action('woocommerce_after_calculate_totals', [$this, 'removeInvalidCoupons'], 11);
        // add a post save action that resets the autoapply couons
        add_action('save_post', [$this, 'resetAutoAppliedCouponsCache'], 10, 3);
        add_action('woocommerce_coupon_is_valid', [$this, 'checkCouponIsValid'], 10, 3);
        add_filter('woocommerce_apply_individual_use_coupon', [$this, 'keepAutoAppliedCouponWhenIndividualCouponHasBeenApplied'], 10, 3);

        $this->offersScheduler->registerEvents();
     //   $this->couponURLManager->manage();
    }
    
    public function keepAutoAppliedCouponWhenIndividualCouponHasBeenApplied(array $couponsToKeep, $appliedCoupon, array $allAppliedCouponCodes) : array
    {
        return Collection::create($allAppliedCouponCodes)->filter(function(string $couponCode) : bool {
            return $this->createCouponsPlusCoupon(new WC_Coupon($couponCode))->canBeAutoApplied();
        })->getValues()->merge(array_values($couponsToKeep))->asArray();
    }
    /**
     * Useful to both manual and auto aplied coupons
     */
    public function checkCouponIsValid(bool $currentIsValid, WC_Coupon $WCCoupon, WC_Discounts $discounts) : bool
    {
        (object) $coupon = $this->createCouponsPlusCoupon($WCCoupon);

        $name = '';
        if (!$coupon->isCouponsPlusCoupon()) {
            return $currentIsValid;
        }

        if ($coupon->canBeAutoApplied() && !$coupon->hasConfigurationContent()) {
            // we'll just let wordpress decide on coupons that were autoapplied but
            // have no coupons+ rows, so that people can choose to autoapply
            // without using coupons+ offers.
            return $currentIsValid;
        }


        return !$coupon->isInvalid();   
    }

    public function onCartReset()
    {
        $this->couponOffersTracker->reset();   
    }

    public function manage($items, \WC_Discounts $wcDiscounts = null)
    {
        /**
         * Determine context: cart vs order
         */
        $object = $wcDiscounts ? $wcDiscounts->get_object() : null;
        $isOrderContext = $object instanceof \WC_Order;

        if ($isOrderContext) {
            $this->dispatchToCouponsForOrder($object, $wcDiscounts);
        } else {
            $this->dispatchToCoupons();
        }

        return $items;
    }

    public function dispatchToCouponsForOrder(\WC_Order $order, \WC_Discounts $wcDiscounts)
    {
        /** @var \WC_Order_Item_Coupon $orderCoupon */
        foreach ($order->get_coupons() as $orderCoupon) {
            $couponCode = $orderCoupon->get_code();
            $wcCoupon = new WC_Coupon($couponCode);
            $coupon = $this->createCouponsPlusCoupon($wcCoupon);

            if ($coupon->isCouponsPlusCoupon() && $coupon->hasConfigurationContent()) {
                $coupon->applyOffersForOrder($order, $wcDiscounts, $this->offersScheduler);
            }
        }
    }

    public function dispatchToCoupons()
    {
        /** @var Coupon $coupon */
        foreach ($this->getAppliedCouponsPlusCoupons()->asArray() as $coupon) {
            $coupon->applyOffers($this->offersScheduler);
        }
    }

    public function autoApplyCoupons()
    {
        (boolean) $withinIncompatibleActions = (doing_action('woocommerce_removed_coupon') ||
                                                doing_action('woocommerce_applied_coupon'));

        if ($withinIncompatibleActions) {
            return;
        }

        /**
         * todo: here we are queryng the whole coupons databse every time the cart is calculated! which is absolutely terrible for performance. Only query the db once per request and cache that result.
         */
        foreach ($this->getCouponsToAutoApply()->asArray() as $couponToAutoApply) {
            /**
             * We need to manually check if it's valid or
             * woocommerce will yell very annoyingly
             * that this coupon is not valid.
             *
             * We don't wann' be showin' no error messages when autoapplying, thanks. 
             */

            if (!$couponToAutoApply->isInvalid() || ($couponToAutoApply->canBeAutoApplied() && !$couponToAutoApply->hasConfigurationContent())) {
                WC()->cart && WC()->cart->apply_coupon($couponToAutoApply->getClassic()->get_code($context = 'edit'));
            }
        }
    }

    public function removeInvalidCoupons()
    {
        if ($this->removingInvalidCoupons) {
            return;
        }

        $this->removingInvalidCoupons = true;

        (object) $invalidCoupons = function(Coupon $coupon) {
            return $coupon->isInvalid();  
        };

        foreach ($this->getAppliedCouponsPlusCoupons()->filter($invalidCoupons)->asArray() as $invalidCoupon) {
            $invalidCoupon->removeCoupon();
        }

        $this->removingInvalidCoupons = false;
    }
    
    protected function getAppliedCouponsPlusCoupons() : Collection
    {
        return $this->couponFactory->createCouponsPlusCouponsOnly(
            new Collection(WC()->cart? WC()->cart->get_coupons() : [])
        );
    }

    public function createCouponsPlusCoupon(WC_Coupon $WCCoupon) : Coupon 
    {
        return CouponRegistry::getInstance()->get($WCCoupon);
    }

    protected function getCouponsToAutoApply() : Collection
    {
        /**
         * This should improve performance quite a bit, and we might improve it further by caching it in the admin side when a coupon is saved.
         */
        return RequestCache::getInstance()->getOrSet('coupons_to_auto_apply', function () {
            (object) $couponsToAutoApply = new \WP_Query([
                'post_type' => 'shop_coupon',
                'meta_key' => Env::getWithPrefix('coupon_auto_apply_is_enabled'),
                'meta_value'   => 'yes',
                'meta_compare' => '=',
                'posts_per_page' => -1
            ]);

            return (new Collection($couponsToAutoApply->posts))->map(function(\WP_Post $coupon) : Coupon {
                return $this->createCouponsPlusCoupon(new WC_Coupon($coupon->ID));
            })->filter(function(Coupon $coupon) : bool {
                return $coupon->code()->isNotEmpty()
                    &&
                    !array_key_exists($coupon->getClassic()->get_code($context = 'edit'), WC()->cart? WC()->cart->get_coupons() : []);
            })->filter(function(Coupon $coupon) : bool {
                // do not auto-apply if its saved in the cart state as incompatible
                return !IncompatibleCartItemState::hasForCoupon($coupon->getClassic());
            });

        });
    }

    /**
     * this one is for tracking the *latest* offers applied, it has to be reset every time because we might apply and offer and then the cart changes and suddenly that offer was not applied anymore because the engine returned a different offer (think tiers (after applying an offer, and after rerunning the engine, the tier that previously matched it no longer does so that previous offer is no longer applied, so we need to reset the ids of the offers every time before calculating totals))
     */
    public function resetGlobalState()
    {
        $this->couponOffersTracker->resetOfferIds();
        // very important to reset notifications!
        Notifications::getInstance()->reset();
    }

    public function resetAutoAppliedCouponsCache(int $postID, \WP_Post $post, bool $update)
    {
        if ($post->post_type !== 'shop_coupon') {
            return;
        }

        RequestCache::getInstance()->clear('coupons_to_auto_apply');
    }
}