<?php

namespace CouponsPlus\App\Offers;

use CouponsPlus\App\Conditions\CartItem;
use CouponsPlus\App\Conditions\ItemsSet;
use CouponsPlus\App\Conditions\PendingCartItem;
use CouponsPlus\App\Coupon\CartComponentsSet;
use CouponsPlus\App\Coupon\Tree\Cart\CartModifiedException;
use CouponsPlus\App\Coupon\Tree\Finders\Sets\OfferItems;
use CouponsPlus\App\Coupon\Tree\Finders\Sets\OffersDataSet;
use CouponsPlus\App\Offers\Abilities\Offers;
use CouponsPlus\App\Offers\OffersScheduler;
use CouponsPlus\App\Offers\OffersSet;
use CouponsPlus\Original\Collections\Collection;

#[\AllowDynamicProperties]
Class OffersSetCollection extends CartComponentsSet implements Offers
{
    protected static $registeredItemsToUse = [];

    public static function registerItemsToUse(OfferItems $offerItems, string $offerId, int $offerInstanceIndex): void
    {
        // only one by combo of offer id and instance index
        static::$registeredItemsToUse[$offerId . '_' . $offerInstanceIndex] = $offerItems;
    }

    public static function getRegisteredItemSetsToUse(): Collection
    {
        return (new Collection(static::$registeredItemsToUse))->map(function (offerItems $offerItems) : array {;
            return $offerItems->getAll()->asArray();
        })->flatten();
    }

    public static function resetRegisteredItemsToUse(): void
    {
        static::$registeredItemsToUse = [];
    }

    public function apply(OffersScheduler $offersScheduler)
    {
        // First, add all pending cart items from all sets in one batch to avoid redundant cart operations
        $this->addAllPendingCartItemsToTheCart();

        foreach ($this->getOffersSets()->asArray() as $offersSet) {
            $offersSet->apply($offersScheduler, true);
        }
    }

    /**
     * Collects all pending cart items from all offer sets and joins them by product key,
     * summing quantities for items that are the same product across different sets (eg: from recursive iterations).
     *
     * @throws CartModifiedException
     */
    protected function addAllPendingCartItemsToTheCart(): void
    {
        (object) $allPendingItems = $this->getAllPendingItemsToAddToCartJoined();

        OffersSet::addPendingItemsToCart($allPendingItems);
    }

    /**
     * Collects all pending items from all offer sets and joins same products by summing their quantities.
     *
     * @return Collection<PendingCartItem>
     */
    protected function getAllPendingItemsToAddToCartJoined(): Collection
    {
        (object) $joinedPendingItems = new Collection([]);

        /** @var OffersSet $offersSet */
        foreach ($this->getOffersSets()->asArray() as $offersSet) {
            (object) $itemsSet = $offersSet->getItemsSet();
            (object) $pendingItems = $itemsSet->getPendingItemsToAddToCart();
            (string) $offerId = $offersSet->getIds()->first() ?? '';

            /** @var PendingCartItem $pendingCartItem */
            foreach ($pendingItems->asArray() as $pendingCartItem) {
                (string) $key = $pendingCartItem->getState('key');

                /** @var PendingCartItem|null $existingPendingCartItem */
                (object) $existingPendingCartItem = $joinedPendingItems->find(function (CartItem $cartItem) use ($key): bool {
                    return $cartItem->getState('key') === $key;
                });

                if ($existingPendingCartItem) {
                    $existingPendingCartItem->setQuantity(
                        $existingPendingCartItem->getQuantity() + $pendingCartItem->getQuantity()
                    );
                } else {
                    // Store the offer ID on the pending item for later use
                    $pendingCartItem->offerIdForAutoAdd = $offerId;
                    $joinedPendingItems->push($pendingCartItem);
                }
            }
        }

        return $joinedPendingItems;
    }

    public function canBeApplied() : bool
    {
        foreach ($this->getOffersSets()->asArray() as $offersSet) {
            if ($offersSet->canBeApplied()) {
                return true;
            }
        }

        return false;
    }

    public function getItemSets() : Collection
    {
        return $this->getItems()->map(function(/*OffersSet | OffersDataSet*/ $offersSet) : ItemsSet {
            return $offersSet->getItemsSet();
        });
    }

    /**
     * This one returns all items used across all offer sets. This is a combination of items to discount and items used to make the offer eligible.
     *
     * Discount and eligible ARE DIFFERENT ITEMS.
     *
     * So for example:
     *
     * -Discounted: 1x Product A
     * -Used to make eligible: 2x Product A
     *
     * Results in 3 x Product A being returned here and not just 2x Product A.
     *
     * @param string|null $excludeOriginId If provided, items from offers with this origin ID will be excluded.
     *                                      This allows virtualized trees (which share the same origin ID) to use the same cart items.
     */
    public function getAllItemsUsed(?string $excludeOriginId = null): Collection
    {
        return $this->getItems()
            ->filter(function (OffersDataSet $offersDataSet) use ($excludeOriginId): bool {
                if ($excludeOriginId === null) {
                    return true;
                }
                return $offersDataSet->getOriginId() !== $excludeOriginId;
            })
            ->map(function (OffersDataSet $offersDataSet) : array {
                return [
                    $offersDataSet->getOfferItems()->getItemsToDiscount(),
                    $offersDataSet->getOfferItems()->getItemsUsedToMakeTheOfferEligibleOnly()
                ];
            })->flatten();
    }

    public function isValid(): bool
    {
        foreach ($this->getItems()->asArray() as $offersSet) {
            if ($offersSet->isValid()) {
                return true;
            }
        }

        return false;
    }     
    /**
     * A collection of OffersSet objects
     */
    public function addOffersSet(OffersSet $offersSet)
    {
        $this->items->push($offersSet);
    }
 
    protected function getInitialItemsCollection(array $items) : Collection
    {
        return (new Collection($items))->filter(function($item) : bool {
            // not ideal at all, but this will have to do for now...
            return $item instanceof OffersSet || $item instanceof OffersDataSet;
        });
    }
     
    public function getOffersSets() : Collection
    {
        return $this->items;
    }

    public function getIds(): Collection
    {
        $ids = new Collection([]);

        /** @var OffersSet $offersSet */
        foreach ($this->getOffersSets()->asArray() as $offersSet) {
            $ids = $ids->concat($offersSet->getIds());
        }

        return $ids;
    }
}