<?php

namespace CouponsPlus\App\Offers\BuiltIn;

use Exception;
use CouponsPlus\App\Offers\Offer;
use CouponsPlus\App\Coupon\CardData;
use CouponsPlus\App\Conditions\ItemsSet;
use CouponsPlus\Original\Environment\Env;
use CouponsPlus\Original\Collections\Collection;
use CouponsPlus\Original\Characters\StringManager;
use CouponsPlus\Original\Collections\MappedObject;
use CouponsPlus\App\Coupon\Tree\Cart\CartModifier;
use CouponsPlus\Original\Collections\Mapper\Types;
use CouponsPlus\App\Coupon\Tree\Finders\Sets\OfferItems;
use CouponsPlus\App\Coupon\Tree\Notifications\Notification;
use CouponsPlus\App\Coupon\Tree\Cart\CartModifiedException;
use CouponsPlus\App\Coupon\Tree\Notifications\Notifications;
use CouponsPlus\App\Coupon\Tree\Notifications\Buttons\ShopURL;
use CouponsPlus\App\Coupon\Tree\Notifications\Buttons\DirectURL;
use CouponsPlus\App\Coupon\Tree\Sets\Factories\PriceSorterFactory;
use CouponsPlus\App\Coupon\Tree\Abilities\OffersItemsSetResolvable;
use CouponsPlus\App\Coupon\Tree\Notifications\Buttons\ButtonFactory;
use CouponsPlus\App\Coupon\Tree\ItemSetsToDiscount\ItemsSetToDiscount;
use CouponsPlus\App\Coupon\Tree\ItemSetsToDiscount\CartItemsToDiscountFinder;
use CouponsPlus\App\Coupon\Tree\ItemSetsToDiscount\ItemsSetToDiscountCollection;
use CouponsPlus\App\Coupon\Tree\Factories\ConditionFilterOrOfferFromDataFactory;
use CouponsPlus\App\Coupon\Tree\ItemSetsToDiscount\ItemSetToDiscountHandlerComposite;
use CouponsPlus\App\Coupon\Tree\ItemSetsToDiscount\CartAdderItemSetToDiscountHandler;
use CouponsPlus\App\Coupon\Tree\ItemSetsToDiscount\ItemsToDiscountToOfferItemsAdapter;
use CouponsPlus\App\Coupon\Tree\ItemSetsToDiscount\AvailableItemSetToDiscountCollectionHandler;
use CouponsPlus\App\Coupon\Tree\ItemSetsToDiscount\ItemSetToDiscountCollectionHandlerComposite;
use CouponsPlus\App\Coupon\Tree\ItemSetsToDiscount\ProductsInCartFinderStrategyAbstractFactory;
use CouponsPlus\App\Coupon\Tree\ItemSetsToDiscount\SpecificProductsInCartFinderStrategyFactory;
use CouponsPlus\App\Coupon\Tree\ItemSetsToDiscount\AdvancedProductsInCartFinderStrategyFactory;
use CouponsPlus\App\Coupon\Tree\ItemSetsToDiscount\IncompleteItemSetToDiscountCollectionHandler;
use CouponsPlus\App\Coupon\Tree\ItemSetsToDiscount\CannotBeAutomaticallyAddedButCanBeAddedManuallyNotificationRegistrarItemSetToDiscountHandler;

/**
 * This is a simple BOGO for the same type of item.
 */
#[\AllowDynamicProperties]
Class BuyXGetY extends Offer implements OffersItemsSetResolvable
{
    public const TYPE = 'BuyXGetY';
    protected CartModifier $cartModifier;
    protected CartItemsToDiscountFinder $cartItemsToDiscountFinder;
    protected ItemSetToDiscountCollectionHandlerComposite $itemsSetToDiscountHandler;

    public static function getName() : string
    {
        return __('Different Products', 'coupons-plus-for-woocommerce');
/*        return __ ('Get Y (Different products)', 'coupons-plus-for-woocommerce');*/
    }

    public static function getDescription() : string
    {
        return __('Apply a discount to specific products');
    }

    /**
     *  Not used in 3.0, but required for the legacy interface.
     */
    public static function getIconUrl(): string
    {
        return '';
    }

    public static function getOptions() : Collection
    {
        return new Collection([
            'mode' => Types::STRING()->withDefault('products')->allowed([
                __('Products', 'coupons-plus-for-woocommerce') => 'products',
                __('Advanced Filters', 'coupons-plus-for-woocommerce') => 'advanced',
/*                __ ('Any Products', 'coupons-plus-for-woocommerce') => 'any', // any items in the cart*/
            ])->meta([
                'alternativeLabels' => [
                    'products' => __('Specific products and quantities', 'coupons-plus-for-woocommerce'),
                    'advanced' => __('Combinations, cat., brands, attributes, etc.', 'coupons-plus-for-woocommerce'),
                ]
            ]),
            /**
             * ------------------------------------------------------------------
             * These are the options for mode: products
             *
             *  A collection of objects with the following structure:
             *  {
             *      'id': number,
             *      'isVariable': bool,
             *      'quantity': 1,
             *      'actions': {
             *          notInCart: {
             *               actions: ('auto_apply'|'show_notification')[],
             *               show_notification: {
             *                   message: string,
             *                   button: {
             *                      text: string
             *                       // the url will be taken from the product's url
             *                  }
             *               }
             *          },
             *          [here possibly after applying discount (show notification), ...etc...]
             *      },
             *      'discount': {
             *           type: 'free' | 'percentage' | 'amount' | 'fixed-price',
             *           amount: number,
             *       }
             *  }
             * ------------------------------------------------------------------
             * ------------------------------------------------------------------
             *  These are the options for mode: advanced
             *   A collection of objects with the structure of ContextData (TestableGroupData) with the children begin Testable data.
             *  JSON items so that we don't parse them if the mode is not advanced to avoid unnecessary overhead.
             *
             *  Example:
             *       [
             *            [
             *                'type' => 'context',
             *                'mode' => 'filter',
             *                'children' => [
             *                    [
             *                        'testableType' => 'filter',
             *                        'type' => InCategories::TYPE,
             *                        'options' => [
             *                            'expectedValues' => [$this->shoes['id'], $this->jackets['id']],
             *                            'inclusionType' => 'allowed'
             *                        ]
             *                    ],
             *                    [
             *                        'testableType' => 'filter',
             *                        'type' => NumberOfItems::TYPE,
             *                        'options' => [
             *                            'quantity' => [
             *                            'type' => 'equals',
             *                            'amount' => 1,
             *                            ]
             *                        ]
             *                    ]
             *                ]
             *            ],
             *           ...other contexts here...,
             *        ]
             * /
             * 'source' => Types::COLLECTION()->withDefault([]),
             * 'discountOptions' => {
             *     'quantity': number, // the amount of items to discount
             *       'discount': {
             *            type: 'free' | 'percentage' | 'amount' | 'fixed-price',
             *            amount: number, // the amount of discount, or the fixed price, ignored if free
             *        }
             *  }
             * ------------------------------------------------------------------
             */
            'data' => Types::COLLECTION()->meta([
                '__allowed' => [
                    'type' => 'remote',
                    'category' => 'products',
                    'where' => 'admin-ajax.php',
                    'path' => '',//'admin-ajax.php?term=red&action=woocommerce_json_search_products_and_variations&security=ee531e9a76',
                    'searchArgumentName' => 'term',
                    'excludeIdsArgumentName' => 'exclude',
                    'arguments' => [
                        'context' => 'edit',
                    ]
                ]
            ]),
            // here potentially the action to take when not in the cart:
            // right now its just add to cart and/or show notification when not possible
            /**
             * 1 - add to cart
             * 2 - show notification
             * 3 - show a selectable list of products
             */
            'whenNotInCart' => [
                'actions' => types::COLLECTION()->withDefault('auto')->allowed(['auto']), // auo for now, but could be a way to let the admin to choose to auto-apply or not (let the user manually add the items to the cart) also wheter to add a mesaage or not, so something like:
                // actions:
                //          a) auto add to cart (when possible)
                //          b) show notification
                //          c) show selectable list of products (when not possible)
                //          d) do nothing
                // and then
                'notification' => [
                    'message' => Types::STRING()->withDefault(
                        $defaultMessage = __("You've unlocked an exclusive offer!", 'coupons-plus-for-woocommerce')
                    )->meta([
                        'name' => __('Notification Message', 'coupons-plus-for-woocommerce'),
                        'placeholder' => StringManager::create(__('* (default)', 'coupons-plus-for-woocommerce'))->replace(
                            '*',
                            $defaultMessage
                        )->get(),
                    ]),
                    'button' => [
                        'text' => Types::STRING()->withDefault(
                            $defaultButtonText =__("Select Reward", 'coupons-plus-for-woocommerce')
                        )->meta([
                            /*'name' => __('Button Text', 'coupons-plus-for-woocommerce'),*/
                            'placeholder' => StringManager::create(__('* (default)', 'coupons-plus-for-woocommerce'))->replace(
                                '*', $defaultButtonText
                            )->get(),
                        ]),
                        'url' => [
                            'type' => Types::STRING()->withDefault(ShopURL::TYPE)->allowed([
                          //      __('Smart', 'coupons-plus-for-woocommerce') => SmartURL::TYPE,
                                __('Shop', 'coupons-plus-for-woocommerce') => ShopURL::TYPE,
                                __('URL', 'coupons-plus-for-woocommerce') => DirectURL::TYPE,
                                //__('Page', 'coupons-plus-for-woocommerce') => PageURL::TYPE, // this works for static pages as well as for products (the id is the same in the db)
                            ])->meta([
                                'name' => __('Button Type', 'coupons-plus-for-woocommerce')
                            ]),
                            'value' => Types::STRING() // id if page, or a real url if url
                        ], // this is an id, can accept a real url or an id
                    ]
                ]
            ]
        ]);
    }

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

    public function afterFullLoad()
    {
        $notificationButtonFactory = new ButtonFactory;
        $productsInCartFinderStrategyFactory = new ProductsInCartFinderStrategyAbstractFactory(Collection::create(
            [
                new SpecificProductsInCartFinderStrategyFactory,
            ])->concat(apply_filters(Env::getWithPrefix('GetY.strategyFactories'), new Collection([])))->asArray());

        /**
         * This finds all the items in the cart that may be eligible to use for the discount.
         */
        $this->cartItemsToDiscountFinder = new CartItemsToDiscountFinder(
            $productsInCartFinderStrategyFactory->create(
                $this->options->mode,
                $this->options->data,
            ),
            new PriceSorterFactory
        );

        $this->itemsSetToDiscountHandler = new ItemSetToDiscountCollectionHandlerComposite([
            /**
             * first the incomplete handler so that if we throw an exception upon adding to the cart,
             * we halt execution and don't get to the next handler
             */
            new IncompleteItemSetToDiscountCollectionHandler(
                new ItemSetToDiscountHandlerComposite([
                        /*new CartAdderItemSetToDiscountHandler(new CartModifier),*/
                        new CannotBeAutomaticallyAddedButCanBeAddedManuallyNotificationRegistrarItemSetToDiscountHandler(
                            new Notification(
                                $this->getID(), // this is enough because each offer has a unique ID regardless of the coupon. There cannot be two offers with the same ID in the same or different coupons. ids are global.
                                $this->options->whenNotInCart->notification->message,
                                $notificationButtonFactory->createFromButtonData(
                                    $this->options->whenNotInCart->notification->button
                                ),
                            ),
                            Notifications::getInstance()
                        ),
                    ]
                )
            ),
            /**
             * Then the complete handler, by this point all items that can possibly be added to the cart have been added.
             */
            new AvailableItemSetToDiscountCollectionHandler($this->coupon)
        ]);
    }

    public function resolveItemsToUse(ItemsSet $filteredItems): OfferItems
    {
        $itemSetsToDiscount = $this->cartItemsToDiscountFinder->findItemsToDiscount($filteredItems);

        // convert the itemstodisount to cart items if they are found or if the aren't found but their mode is "auto-apply"
        // all of these sets are PER a single passed $itemsset, so for the offeriitems:
        //      +used to make the offer eiligible: $filteredItems -always this one and only one per run.
        //      +itemstodisocunt: the ones converted from the $itemSetsToDiscount
        // we also need to use this one when querying the cart: ItemsSet::createFromAllEligibleCartItemsDuringDiscovery()
        // so as to exclude the items in recurisive mode.
        (object) $offerItemsAdapter = new ItemsToDiscountToOfferItemsAdapter($filteredItems);

        return $offerItemsAdapter->adapt($itemSetsToDiscount);
    }

    /**
     * @param ItemsSet $itemsSet
     * @param OfferItems $offerItems
     * @throws CartModifiedException
     * @throws Exception
     */
    public function apply(ItemsSet $itemsSet)
    {
        /**
         * I dont know why now but the offerdata is passing the opposite items: the itemsused are passed as the ones to discount and the items to discount are passed as the ones used.
         */
        $filteredItems = $itemsSet->getOfferItems()->getItemsUsedToMakeTheOfferEligibleOnly();
        /**
         * So, unfortunately, we do have to run this twice.
         * For now, at least, let me get a poc and then we can optimize later.
         */
        (object) $itemsSetToDiscountCollection = $this->cartItemsToDiscountFinder->findItemsToDiscount($filteredItems);
        /**
         * This handler deals with addition to the cart, notifications, discount registrations, etc.
         */
        $this->itemsSetToDiscountHandler->handle($itemsSetToDiscountCollection);
    }

    private function registerSetDiscounts(ItemsSetToDiscountCollection $itemsToDiscount)
    {
        $itemsToDiscount->getSetsThatCanBeFullyApplied()->forEvery(function(ItemsSetToDiscount $itemsSetToDiscount) {
            $this->registerDiscounts($itemsSetToDiscount->itemsSet);
        });
    }

    /**
     * This is a blatant duplicate (i mean a literal 1:1 copy) of the one in BuyXGetX (need to refactor it).
     */
    private function registerDiscounts(ItemsSet $itemsToDiscount)
    {
        $offerFactory = new ConditionFilterOrOfferFromDataFactory($this->coupon);

        if ($this->options->discount->type->is('fixed-price')) {
            $offerData = new CardData(
                FixedPriceInternal::TYPE,
                new MappedObject([
                    'amount' => $this->options->discount->amount,
                ])
            );
        } else {
            // todo: let's refactor the FREE logic to the discount offer so that it is a native option of discount
            $isFree = $this->options->discount->type->is('free');

            $offerData = new CardData(
                Discount::TYPE,
                new MappedObject([
                        'type' => $isFree ? 'percentage' : $this->options->discount->type,
                        'amount' => $isFree ? 100 : $this->options->discount->amount,
                        'scope' => 'filtereditems',
                    ]
                )
            );
        }
        $discount = $offerFactory->create(
            'offer',
            $offerData
        );

        $discount->apply($itemsToDiscount);
    }
}

// all products in the cart (nothing added)
// requires product a (regular) x1 and cart dont have it (adds it)
// requires product a (variable base) x1 and cart dont have it (does NOT add it)
// requires product a (specific variation) x1 and cart dont have it (adds it)

// requires product a (regular) x2 and cart dont have it (adds 2)
// requires product a (regular) x1 and cart has only 1 (adds 1)
// requires product a (regular) x3 and cart has 1 (adds 2)

//requires product a x1 and product b x1 and cart has product a and b (add nothing)
//requires product a x1 and product b x1 and cart has neither (adds both)
//requires product a x1 and product b x1 and cart has product a (adds product b)
// requires product a x1 and product b x1 and cart has product b (adds product a)
//requires product a x1 and product b x3 and cart has product a and 1 product b x1 (adds product b x2)
//requires product a x1 and product b x3 and cart has product b x1 (adds product a and product b x2)

// combination of the above but with required variable base producrs