<?php

namespace CouponsPlus\App\Offers\BuiltIn;

use CouponsPlus\App\Offers\HardLimits;
use CouponsPlus\App\Conditions\BuiltIn\Filters\NoItems;
use CouponsPlus\App\Conditions\CartItem;
use CouponsPlus\App\Conditions\ItemsSet;
use CouponsPlus\App\Coupon\CardData;
use CouponsPlus\App\Coupon\TestableData;
use CouponsPlus\App\Coupon\Tree\Abilities;
use CouponsPlus\App\Coupon\Tree\Abilities\ErrorHandler;
use CouponsPlus\App\Coupon\Tree\Finders\Sets\OfferItems;
use CouponsPlus\App\Coupon\Tree\Sets\Factories\PriceSorterFactory;
use CouponsPlus\App\Coupon\Tree\Factories\ConditionFilterOrOfferFromDataFactory;
use CouponsPlus\App\Coupon\Tree\Factories\ContextFromTestableGroupDataFactory;
use CouponsPlus\App\Coupon\Tree\Factories\FilterableGroupFactory;
use CouponsPlus\App\Coupon\Tree\Factories\OnErrorJSONFactory;
use CouponsPlus\App\Coupon\Tree\Factories\StandaloneProductionTestableGroupFactory;
use CouponsPlus\App\Coupon\Tree\Factories\TestableGroupDataFromJSONFactory;
use CouponsPlus\App\Coupon\Tree\Factories\TestableGroupDataFromStandardObjectFactory;
use CouponsPlus\App\Coupon\Tree\Factories\TestableGroupFactoryFromData;
use CouponsPlus\App\Coupon\Tree\TestableGroupData;
use CouponsPlus\App\Helpers\Currency;
use CouponsPlus\App\Offers\CouponCartDiscountAmount;
use CouponsPlus\App\Offers\CouponItemDiscountAmount;
use CouponsPlus\App\Offers\Offer;
use CouponsPlus\Original\Collections\Collection;
use CouponsPlus\Original\Collections\MappedObject;
use CouponsPlus\Original\Collections\Mapper\Types;
use CouponsPlus\Original\Environment\Env;
use Mattiasgeniar\Percentage\Percentage;

use function CouponsPlus\App\Coupon\Tree\TestableHooks\usingSorter;

#[\AllowDynamicProperties]
Class Discount extends Offer
{
    const TYPE = 'Discount';
    
    protected $quantitiesApplied = 0;
    /**
     * @var string
     */
    protected $uniqueKey;

    public static function getName() : string
    {
        return __('Discount', 'coupons-plus-for-woocommerce');
    }

    public static function getDescription() : string
    {
        return __('Sets a discount on some items or the whole cart.', 'coupons-plus-for-woocommerce');
    }

    public static function getOptions() : Collection
    {
        return new Collection([
            'type' => Types::STRING()->withDefault('percentage')
                                    ->meta([
                                        'name' => __('Type', 'coupons-plus-for-woocommerce')
                                    ])
                                    // PLEASE NOTE: amount CURRENTLY ONLY AVAILABLE TO cartsubtotal (AS CART TOTAL)
                                     ->allowed([
                                        '%' => 'percentage', 
                                        (Currency::$currencies[get_woocommerce_currency()] ?? Currency::$currencies['USD'])['symbol'] => 'amount',
                                    ]),
            'amount' => Types::FLOAT,
            'scope' => Types::STRING()->withDefault('filtereditems')
                                             ->allowed([
                                                __('Filtered items', 'coupons-plus-for-woocommerce')=> 'filtereditems',
                                                __('Cart subtotal', 'coupons-plus-for-woocommerce')=> 'cartsubtotal',
                                              ]),
            'customFilteredItemsSourceMap' => Types::STRING()->withDefault(''
                /**
                 * This is a Testable Composite Group Thank god (because of its amazing flexibility)!
                 */
                //this needs a context but it's created in the dashboard
                //(new DataExporter)->export(SimpleColumn::createFromOptions('{"testableType": "filters"}', new \WC_Coupon()))
            ),
            'customFilteredItemsSourceMapOptions' => [
                'notifications' => [
                    'success.NoItems' => __("Congratulations! You've unlocked savings on select items - shop eligible products to get your discount", 'coupons-plus-for-woocommerce')
                ]
            ],
            // # items limit
            'limit' => [
                'isEnabled' => Types::BOOLEAN()->withDefault(false)->meta([
                    // pre 3.0
                    'disabled' => __('With no limit', 'coupons-plus-for-woocommerce'),
                    'enabled' => __('Limit to:', 'coupons-plus-for-woocommerce'),
                    // 3.0+
                    'labels' => [
                        'true' => __('Disable limit', 'coupons-plus-for-woocommerce'),
                        'false' => __('Set limit', 'coupons-plus-for-woocommerce'),
                    ]
                ]),
                /**
                 * New in 3.0
                 */
                'type' => Types::STRING()->withDefault('fixed')
                                         ->allowed([
                                            __('Fixed quantity', 'coupons-plus-for-woocommerce')=> 'fixed',
                                            __('1/2 the items', 'coupons-plus-for-woocommerce')=> 'half',
                                            __('Pattern: X every Y', 'coupons-plus-for-woocommerce')=> 'pattern',
                                             // 2 every 3 items - pattern-based grouping
                                        ]),
                // only when type is 'pattern'
                'pattern' => [
                    // this one is the 2 in "2 every 3"
                    'discounted' => Types::INTEGER()->withDefault(1)->meta([
                        'labels' => [
                            'input' => [
                                'top' => __('Apply to', 'coupons-plus-for-woocommerce'),
                                'bottom' => __('items', 'coupons-plus-for-woocommerce'),
                            ]
                        ]
                    ]),
                    // this one is the 3 in "2 every 3"
                    'groupSize' => Types::INTEGER()->withDefault(3)->meta([
                        'labels' => [
                            'input' => [
                                'top' => __('Of Every', 'coupons-plus-for-woocommerce'),
                                'bottom' => __('items', 'coupons-plus-for-woocommerce'),
                            ]
                        ]
                    ]),
                ],
                'amount' => Types::INTEGER()->withDefault(0), // MAXIMUM WILL BE performed,
                'orderBy' => Types::STRING()->withDefault('lowestprice')
                                            ->allowed(['lowestprice', 'highestprice'])
            ],
            // $ limit
            'hardLimit' => [
                'isEnabled' => Types::BOOLEAN()->withDefault(false)->meta([
                    'labels' => [
                        'true' => __('Disable maximum amount', 'coupons-plus-for-woocommerce'),
                        'false' => __('Set maximum amount to discount', 'coupons-plus-for-woocommerce'),
                    ]
                ]),
                'amount' => Types::FLOAT, // MAXIMUM WILL BE performed,
            ]
            // we need to do the partial discount too (haldf the amount etc)
        ]);
    }

    public function setCoupon(\WC_Coupon $coupon)
    {
        parent::setCoupon(...func_get_args());

        $this->testableGroupFactory = new StandaloneProductionTestableGroupFactory($this->coupon);
    }

    public function apply(ItemsSet $itemsSet)
    {
        (object) $itemsSet = $this->getItemsSet($itemsSet);

        $this->generateUNIQUEKeyForThisDiscount();
        $this->registerHardLimitForThisInstance();

        switch ($this->options->type) {
            case 'percentage':
                $this->applyPercentage($itemsSet);
                break;
            case 'amount':
                $this->applyFixedAmount($itemsSet);
                break;
        }
    }

    protected function applyPercentage(ItemsSet $itemsSet)
    {
        $this->applyDiscountToEachProduct($itemsSet, function(CartItem $cartItem, float $itemsTotalPrice, int $quantity) : float {
            (float) $percentageAmount = $this->options->amount;

            return Percentage::of($percentageAmount, $itemsTotalPrice);
        });  
    }
    
    protected function applyFixedAmount(ItemsSet $itemsSet)
    {
        switch ($this->options->scope) {
            case 'filtereditems':
                $this->applyFixedAmountToItems($itemsSet);
                break;
            case 'cartsubtotal':
                (object) $cartDiscountAmount = new CouponCartDiscountAmount($this->coupon);

                $cartDiscountAmount->addAmount($this->options->amount);
                break;
        }
    }

    protected function applyFixedAmountToItems(ItemsSet $itemsSet)
    {
        $this->applyDiscountToEachProduct($itemsSet, function(CartItem $cartItem, float $itemsTotalPrice, int $quantity, $individualPrice) : float {
            // this comment stays for when debugging.
            //var_dump('product_id', $cartItem->getState('product_id'), '$itemsTotalPrice', $itemsTotalPrice, '$quantity', $quantity, '$this->options->amount', $this->options->amount, 'then:', $quantity * $this->options->amount, 'RESULT', ($quantity * $this->options->amount));
            //var_dump('________________');
            //

            return ($quantity * min($this->options->amount, $individualPrice));
        });  
    }

    protected function applyDiscountToEachProduct(ItemsSet $itemsSet, callable $getDiscount)
    {
        $storerType = $this->hasQuantityLimit()? PriceSorterFactory::CHEAPEST : PriceSorterFactory::NONE;

        $flattened = usingSorter((new PriceSorterFactory)->create($storerType), function () use ($itemsSet) {
            return $itemsSet->splitPublicItems()->getSortedItems();
        });

        if ($this->hasQuantityLimit()) {
            $flattened = $flattened->first($this->getLimitAmount($itemsSet));
        }

        $ids = $flattened->map(function(CartItem $item) {
            return $item->getState('product_id');
        })->asArray();
        foreach ($flattened->asArray() as $cartItem) {
            (integer)$quantity = 1;// $this->getQuantitiesOfItemToApply($cartItem);
            (float) $individualPrice = $cartItem->getProduct()->get_price($context = 'edit');
            (float) $itemsTotalPrice = $individualPrice * $quantity;

            if ($quantity && $this->canApplyDiscountToCurrentItem($cartItem)) {
                (object) $couponDiscount = new CouponItemDiscountAmount($this->coupon);
                $couponDiscount->addCartItemKey($cartItem->getState('key'));
                // this key identifies this unique Discount for all CouponItemDiscountAmount instances
                $couponDiscount->setUniqueKey($this->uniqueKey);

                $intendedDiscount = $getDiscount($cartItem, $itemsTotalPrice, $quantity, $individualPrice);

                $couponDiscount->addAmount(
                    $this->getFinalItemDiscount($intendedDiscount),
                    $quantity
                );

/*                if ($this->options->hardLimit->isEnabled) {
                    $couponDiscount->setHardLimit($this->options->hardLimit->amount);
                }*/
            }
        }  
    }

    /**
     * @deprecated This is no longer used and is set to be removed in the future.
     */
    protected function getQuantitiesOfItemToApply(CartItem $cartItem) : int
    {
        (integer) $quantityToApply = 0;

        # limiting is not supported on cart subtotal
        if (!$this->options->scope->is('cartsubtotal') && $this->options->limit->isEnabled) {
            if ($this->quantitiesApplied < $this->options->limit->amount) {
                $quantityToApply = min($cartItem->getState('quantity'), $this->options->limit->amount - $this->quantitiesApplied);
            }
        } else {
            $quantityToApply = $cartItem->getState('quantity');
        }

        $this->quantitiesApplied += $quantityToApply;

        return $quantityToApply;
    }
    

    protected function getItemsSet(ItemsSet $itemsSet) : ItemsSet
    {
        switch ($this->options->scope) {
            case 'filtereditems':
                return $itemsSet;
                break;
            case 'customfiltereditems':
                return $this->getCustomFilteredItems();
                break;
            case 'cartsubtotal':
                return ItemsSet::createFromAllCartItems();
                break;
        }
    }

    protected function getCustomFilteredItems() : ItemsSet
    {
        /*(object) $testableGroupDataFactory = new OnErrorJSONFactory(
            new TestableGroupDataFromJSONFactory(
                new TestableGroupDataFromStandardObjectFactory
            )
        );

        (object) $testableGroupFactory = new TestableGroupFactoryFromData(
            new FilterableGroupFactory,
            new ContextFromTestableGroupDataFactory(
                new ConditionFilterOrOfferFromDataFactory($this->coupon)
            )
        );

        $testableGroupDataFactory->setErrorHandler(new Class Implements ErrorHandler {
            public function onError(\Throwable $exception) : Abilities\TestableGroupData
            {
                return new TestableGroupData(
                    $type = 'AND',
                    $mode = 'filter',
                    $children = [
                        new TestableGroupData(
                            $type = 'context',
                            $mode = 'filter',
                            $children = [
                                new TestableData(
                                    $testableType = 'filter',
                                    new CardData(
                                        $type = NoItems::TYPE,
                                        $options = new MappedObject([])
                                    )
                                )
                            ]
                        ),
                    ]
                );
            } 
        });

        (object) $testableGroup = $testableGroupFactory->create(
            $testableGroupDataFactory->create(
                $this->options->customFilteredItemsSourceMap->get()
            )
        );*/

        (object) $testableGroup = $this->testableGroupFactory->create(
            $this->options->customFilteredItemsSourceMap->get()
        );

        return $testableGroup->filterSet(ItemsSet::createFromAllCartItems());
    }
    

    public static function getIconUrl() : string
    {
        return Env::directoryURI().'/storage/icons/offers/Discount.svg';   
    }

    protected function generateUNIQUEKeyForThisDiscount()
    {
        $this->uniqueKey = uniqid(wp_rand(), true);
    }

    private function hasQuantityLimit()
    {
        if ($this->options->scope->is('cartsubtotal')) {
            return false;
        }
        return $this->options->limit->isEnabled;
    }

    private function getLimitAmount(ItemsSet $itemsSet)
    {
        /**
         * Because of potential confusion, this is only applied to filtered items
         */
        if (!$this->options->limit->isEnabled || !$this->options->scope->is('filtereditems')) {
            return PHP_INT_MAX; // no limit, so we can apply the discount to all items
        }

        // now we need to figure out the actual amount to limit by
        switch ($this->options->limit->type) {
            case 'fixed':
                return $this->options->limit->amount;
            case 'half':
                return floor($itemsSet->getTotalQuantity() / 2);
            case 'pattern':
                /**
                 * Example: 2 for every 3 (discounted: 2, groupSize: 3)
                 * We got 11 items
                 */
                $totalItems = $itemsSet->getTotalQuantity(); // 11
                // so we'll get the modulus and subtract that from the total and the divide that against our group size
                $remainder = $totalItems % $this->options->limit->pattern->groupSize; //2, because if we got: 11 total and the group size is 3, then the reminder is 2 (9 items which represent 3 groups and the remainder is 2 because we needed another 1 to complete another group)
                $totalGroupsSize = $totalItems - $remainder; // 9, because we have 11 items, and we need to remove the 2 that are left out of the groups

                $totalNumberOfGroups = $totalGroupsSize / $this->options->limit->pattern->groupSize; // 9 / 3 = 3 groups

                return $totalNumberOfGroups * $this->options->limit->pattern->discounted; // 3 * 2 = 6 items will be discounted
        }
    }

    /**
     * The hard limit is registered for this instance of the Discount
     * It is NOT registered by offer type (eg: Discount::TYPE, ShippingDiscount::TYPE, etc))
     * because we may have several instances of the same offer type at once, each with its own hard limit.
     *
     * We don't set it individually ib the coupon item discount amount because this limit is shared across all discounted
     * items from this instance
     *
     * This will be refactord in the future to change from global state to a dependent injected state
     */
    private function registerHardLimitForThisInstance()
    {
        if (!$this->options->hardLimit->isEnabled) {
            return;
        }

        // get the HardLimit global instance (ugh, global)
        $hardLimits = HardLimits::getInstance();
        // then register this offer's unique key
        $hardLimits->register(
            $this->uniqueKey,
            $this->options->hardLimit->amount,
        );
        // that's it, then we can use this instance when applying discounts individually to see if the limit has been reached and if so
        // stop registering items so that they may be used in other filters in the future when we add the functionality of separating items that have been discounted from those that haven't
    }

    /**
     * it's very clear to me that this (along with static::getFinalItemDiscount() needs to be refactored
     * to its own class, a class that is built around a cart item (cartItem::canApplyDiscountTo, cartItem::applyDiscount, etc.))
     *so, like a nice todo: refactor this to a more specific cart item discount class
     */
    protected function canApplyDiscountToCurrentItem(CartItem $cartItem): bool
    {
        $hardLimit = HardLimits::getInstance()->get($this->uniqueKey);

        if (!$hardLimit) {
            return true; // no hard limit registered, so we can apply the discount
        }

        return !$hardLimit->hasReachedHardLimit();
    }

    private function getFinalItemDiscount(float $intendedDiscount) : float
    {
        $hardLimit = HardLimits::getInstance()->get($this->uniqueKey);

        if (!$hardLimit) {
            return $intendedDiscount; // no hard limit registered, so we can apply the discount
        }

        return $hardLimit->requestAndUpdateAvailableAmount($intendedDiscount);
    }
}