<?php

namespace CouponsPlus\App\Coupon\Tree;

use JsonSerializable;
use CouponsPlus\App\Offers\Offer;
use CouponsPlus\App\Coupon\CardData;
use CouponsPlus\App\Conditions\Filter;
use CouponsPlus\App\Conditions\ItemsSet;
use CouponsPlus\App\Offers\BuiltIn\BuyXGetX;
use CouponsPlus\App\Offers\BuiltIn\BuyXGetY;
use CouponsPlus\App\Offers\BuiltIn\Discount;
use CouponsPlus\App\Offers\OffersRegistrator;
use CouponsPlus\Original\Collections\Collection;
use CouponsPlus\Original\Characters\StringManager;
use CouponsPlus\App\Conditions\FiltersRegistrator;
use CouponsPlus\App\Coupon\Tree\Abilities\NodeData;
use CouponsPlus\App\Conditions\ConditionsRegistrator;
use CouponsPlus\App\Coupon\Tree\Sets\NoSpecialSorting;
use CouponsPlus\App\Conditions\Abilities\TestableComposite;
use CouponsPlus\App\Conditions\BuiltIn\Filters\NumberOfItems;
use CouponsPlus\App\Coupon\Tree\Sets\Abilities\HasItemsSetSorter;
use CouponsPlusPro\App\Coupon\Tree\Finders\BottomToTopNodeFinder;
use CouponsPlus\App\Coupon\Tree\Sets\Factories\PriceSorterFactory;
use CouponsPlusPro\App\Coupon\Tree\Finders\FirstPassingNodeFinder;
use CouponsPlus\App\Coupon\Tree\Sets\Abilities\ItemsSetSorterInterface;
use CouponsPlusPro\App\Coupon\Tree\Finders\FilterReturningTheMostItemsNodeFinder;
use CouponsPlus\App\Coupon\Tree\Data\Transformers\Abilities\TreeNodeAlgorithmResolver;

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

class Branch implements Abilities\Branch, JsonSerializable, HasItemsSetSorter
{
    protected ?NodeData $node = null;
    protected ?object $extraData;
    protected PriceSorterFactory $priceSorterFactory;
    /** @property Collection */
    private $children;
    /** @property StringManager */
    private $type;

    public function __construct(string $type, array $children, object $extraData = null)
    {
        $this->children = new Collection($children);
        $this->type = new StringManager($type);
        $this->extraData = $extraData;

        $this->children->forEvery(function ($child) {
            if (method_exists($child, 'setParent')) {
                $child->setParent($this);
            }
        });
        $this->priceSorterFactory = new PriceSorterFactory;
    }

    public function setNode(NodeData $nodeData): void
    {
        $this->node = $nodeData;
    }

    public function getNode(): ?NodeData
    {
        return $this->node;
    }

    public function getChildren(): Collection
    {
        return $this->children;
    }

    public function getType(): StringManager
    {
        if ($this->type->is('tiered')) {
            return new StringManager($this->figureOutAlgorithm());
        }

        return $this->type;
    }

    public function is($type): bool
    {
        return $this->type->is($type);
    }

    #[\ReturnTypeWillChange]
    public function jsonSerialize() : array
    {
        return get_object_vars($this);
    }

    /**
     * This definitely needs to be refactored because:
     * -sorter logic for every branch type
     * -remove the hard-coded dependency (PriceSorterFactory)
     */
    public function getItemsSorter(): ItemsSetSorterInterface
    {
        /**
         * This is a tiered branch so no sorting needed right now
         * Sorting will be set individually on each tier
         */
        if ($this->type->isNot('offers')) {
            return new NoSpecialSorting();
        }

        $hasBogos = $this->children->find(
        /**
         * @param Offer|Abilities\CardData $offer
         * @return bool
         */ function ($offer) : bool {
            return in_array((string) $offer->getType(), [BuyXGetX::TYPE, BuyXGetY::TYPE], true);
        });

        $priceSorterFactory = new PriceSorterFactory;

        if ($hasBogos) {
            return $priceSorterFactory->create(PriceSorterFactory::MOST_EXPENSIVE);
        }

        /** @var Discount|null $discount */
        $discount = $this->children->find(
        /**
         * @param Offer|Abilities\CardData $offer
         * @return bool
         */ function ($offer) : bool {
            return ((string) $offer->getType()) === Discount::TYPE;
        });

        if ($discount) {
            // **DO NOT** REMOVE THIS THINKING YOU COULD SORT THE ITEMS IN THE OFFER BECAUSE:
            // if the testable has a limit (eg: NUmberOfItems equals 2)
            // when getting the subset, we need to have the order here before it gets to the discount offer
            $options = $discount instanceof CardData? $discount->getOptions() :$discount->getLoadedOptions();

            if ($options->scope->is('filtereditems')) {
                return $priceSorterFactory->create(PriceSorterFactory::CHEAPEST);
            }
        }

        $hasThatRequireCheapest = $this->children->find(
        /**
         * @param Offer|Abilities\CardData $offer
         * @return bool
         */ function ($offer) : bool {
            $have = OffersRegistrator::get()->getOffersThatAlwaysRequireCheapestItemsFirst()->map(function (string $Offer) : string {
                return $Offer::TYPE;
            })->have(
                $offer->getType()
            );

            return $have;
        });

        if ($hasThatRequireCheapest) {
            return $priceSorterFactory->create(PriceSorterFactory::CHEAPEST);
        }

        return new NoSpecialSorting();
    }

    private function figureOutAlgorithm() : string
    {
        $tierType = $this->extraData->tierType; // 'numeric', 'fixed', etc.
        $testableType = $this->extraData->baseTestable->testableType; // 'filter' | 'condition'
        $componentType = $this->extraData->baseTestable->type; // 'NumberOfItems', 'InCategories', etc.
        $ComponentRegistrator = $testableType === 'filter' ? FiltersRegistrator::class : ConditionsRegistrator::class;
        $component = $ComponentRegistrator::get()->getByType($componentType);

        if ($resolvedType = $this->tryToResoleCustomAlgorithm($component)) {
            return $resolvedType;
        }

        if ($tierType === 'numeric') {
            if ($testableType === 'filter') {
                return FilterReturningTheMostItemsNodeFinder::TYPE;
            }

            if ($testableType === 'condition') {
                return BottomToTopNodeFinder::TYPE;
            }
        }

        // default
        return FirstPassingNodeFinder::TYPE;
    }

    private function tryToResoleCustomAlgorithm(string $filter) : ?string
    {
        if (is_a($filter, TreeNodeAlgorithmResolver::class, true) && $resolvedType = $filter::resolveTreeNodeAlgorithm($this)) {
            return $resolvedType;
        }

        return null;
    }

    public function getExtraData(): ?object
    {
        return $this->extraData;
    }

    /**
     * @deprecated This was never really implemented in favor of filterItemsSetQuantityBeforePassingToBranch()
     */
    public function getItemsToExclude(ItemsSet $itemsSet): Collection
    {
        (object) $itemsSetToExclude = new Collection([]);

        if (!$this->is('offers')) {
            return $itemsSetToExclude;
        }

        // if we were only passed one item, ignore this, don't exclude anything
        // because otherwise this whole tree will be considered invalid if a bogo offer is present,
        // and we need to run the bogo offer for potential notification purposes
        if ($itemsSet->getTotalQuantity() <= 1) {
            return $itemsSetToExclude;
        }

        (object) $getXOffer = $this->children->find(
            /** * @param Offer|Abilities\CardData $offer */
        function ($offer) : bool {
            return $offer->getType() === BuyXGetX::TYPE;
        });

        (object) $getYOffer = $this->children->find(
        /*** @param Offer|Abilities\CardData $offer */
        function ($offer) : bool {
            return $offer->getType() === BuyXGetX::TYPE;
        });

        /**
         * This should be on its own object buy anyway...
         */
        if ($getXOffer) {
            // so now let's re-query the given items BUT let's sort by cheapest first since we are excluding the gifted items
            usingSorter(
                $this->priceSorterFactory->create(PriceSorterFactory::CHEAPEST),
                function () use ($getXOffer, $itemsSet, $itemsSetToExclude) {
                    $options = $getXOffer instanceof CardData? $getXOffer->getOptions() : $getXOffer->getLoadedOptions();

                    $itemsSetToExclude->push(
                        $itemsSet->selectQuantityOrdered('range', $options->quantity)
                    );
                }
            );
        }

        // todo: getYOffer exclusion

        return $itemsSetToExclude;
    }

    /**
     * Test in BuyXGetXTest and BuyXGetYTest
     */
    public function filterItemsSetContextQuantityBeforePassingToBranch(ItemsSet $filteredItemsSet, TestableGroup $testableGroup): ItemsSet
    {
        // only works for bogo branches currently
        if (!$this->isBogoBranch()) {
            return $testableGroup->filterSet($filteredItemsSet);
        }

        (object) $testableGroupQuantityFilter = $testableGroup->testableGroups->find(
        /**
         * @param Filter|Abilities\CardData $filter
         * @return bool
         */ function ($filter): bool {
                // gettype might return a string
                return (string) $filter->getType() === NumberOfItems::TYPE;
            }
        );

        if (!$testableGroupQuantityFilter) {
            // we don't have a number of items, so let's add one that picks only 1 item
            (object) $numberOfItems = new NumberOfItems([
                'quantity' => [
                    'type' => 'equals',
                    'amount' => 1,
                ]
            ]);

            return $numberOfItems->filterSet($filteredItemsSet);
        }

        // we have a number of items filter
        $options = $testableGroupQuantityFilter instanceof CardData? $testableGroupQuantityFilter->getOptions() : $testableGroupQuantityFilter->getLoadedOptions();

        // if its equal, just pass it through
        if ($options->quantity->type->is('equals')) {
            return $testableGroup->filterSet($filteredItemsSet);
        }

        if ($options->quantity->type->is('minimum')) {
            // if its minimum, we need to adjust it to equal that amount
            (object) $numberOfItems = new NumberOfItems([
                'quantity' => [
                    'type' => 'equals',
                    'amount' => $options->quantity->amount,
                ]
            ]);

            return $numberOfItems->filterSet($filteredItemsSet);
        }

        if ($options->quantity->type->is('maximum')) {
            // check if we have added eligible items already, if so, check that they do not exceed the maximum

            // maximum so just give me one, the maximum defines the max eligible items.
            // this might be inside a loop and every time the eligible and discounted items are removed from the set
            (object) $numberOfItems = new NumberOfItems([
                'quantity' => [
                    'type' => 'equals',
                    'amount' => 1,
                ]
            ]);

            return $numberOfItems->filterSet($filteredItemsSet);
        }

        if ($options->quantity->type->is('range')) {
            // here if: has already added elegible items, then dont check for the min, just same behaviour as maximum
            //      if: has not added elegible items, then check that it meets the minimum, if it does, just the exact same as maximum
            // range so lets see if we fit the minimum, if so, just give me as if this was max (one item)

            if ($filteredItemsSet->getTotalQuantity() >= $options->quantity->range->minimum) {
                (object) $numberOfItems = new NumberOfItems([
                    'quantity' => [
                        'type' => 'equals',
                        'amount' => 1,
                    ]
                ]);

                return $numberOfItems->filterSet($filteredItemsSet);
            }

            // we don't meet the minimum, so let's just return an empty set
            return new ItemsSet();
        }

        return $testableGroup->filterSet($filteredItemsSet);
    }

    public function isBogoBranch() : bool
    {
        if (!$this->is('offers')) {
            return false;
        }

        return (bool) $this->children->find(
            /** * @param Offer|Abilities\CardData $offer */
            function ($offer) : bool {
                return in_array((string) $offer->getType(), [BuyXGetX::TYPE, BuyXGetY::TYPE], true);
        });
    }
}