<?php

declare(strict_types=1);

namespace KupShop\GraphQLBundle\ApiAdmin\Util;

use KupShop\CatalogBundle\ProductList\ProductList;
use KupShop\GraphQLBundle\ApiAdmin\Types\Purchase\Input\PurchaseCalculateInput;
use KupShop\GraphQLBundle\Exception\GraphQLNotFoundException;
use KupShop\GraphQLBundle\Exception\GraphQLValidationException;
use KupShop\KupShopBundle\Context\ContextManager;
use KupShop\KupShopBundle\Context\CurrencyContext;
use KupShop\KupShopBundle\Context\UserContext;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Functional\Mapping;
use KupShop\KupShopBundle\Util\Price\Price;
use KupShop\KupShopBundle\Util\Price\PriceCalculator;
use KupShop\OrderDiscountBundle\Util\DiscountManager;
use KupShop\OrderingBundle\Entity\Purchase\ProductPurchaseItem;
use KupShop\OrderingBundle\Entity\Purchase\PurchaseState;
use Query\Operator;
use Query\Product;

class PurchaseUtil
{
    public function __construct(
        protected ProductList $productList,
        protected \KupShop\OrderingBundle\Util\Purchase\PurchaseUtil $purchaseUtil,
        protected DiscountManager $discountManager,
        protected ContextManager $contextManager,
    ) {
    }

    public function calculatePurchaseState(PurchaseCalculateInput $input): \KupShop\GraphQLBundle\ApiAdmin\Types\Purchase\PurchaseState
    {
        if (empty($input->items)) {
            throw new GraphQLValidationException('Argument "items" cannot be empty!');
        }

        $purchaseState = $this->contextManager->activateContexts([UserContext::class => $input->userId], function () use ($input) {
            $productList = clone $this->productList;
            $productList->setVariationsAsResult(true);

            $purchaseState = $this->createPurchaseState();

            $productListProducts = [];
            $productListCodes = [];

            // pripravim si produkty, ktere budu nacitat pres product list
            foreach ($input->items as $item) {
                if (empty($item->productId) && empty($item->code)) {
                    throw new GraphQLValidationException('Invalid "PurchaseItemInput"! One of these fields is required: productId, code');
                }

                if ($item->code) {
                    $productListCodes[] = $item->code;
                }

                if ($item->productId) {
                    $productListProducts[$item->productId] = $productListProducts[$item->productId] ?? null;
                    if ($item->variationId) {
                        $productListProducts[$item->productId][] = $item->variationId;
                    }
                }
            }

            // specka pro nacteni produktu / variant podle idcek
            if (!empty($productListProducts)) {
                $productList->andSpec(Product::productsAndVariationsIds($productListProducts));
            }

            // specka pro nacteni produkt / variant podle kodu
            if (!empty($productListCodes)) {
                $productList->andSpec(Operator::orX(
                    Operator::inStringArray($productListCodes, 'p.code'),
                    Operator::inStringArray($productListCodes, 'pv.code')
                ));
            }

            $products = $productList->getProducts()->toArray();
            $productsByCode = Mapping::mapKeys($products, fn ($k, $v) => [$v->variationCode ?? $v->code, $v]);

            $currencyContext = Contexts::get(CurrencyContext::class);
            $currency = $currencyContext->getActive();

            // dohledam Product a pridam PurchaseItem do PurchaseStatu
            foreach ($input->items as $item) {
                $product = null;
                if ($item->code) {
                    if (!($product = ($productsByCode[$item->code] ?? null))) {
                        throw new GraphQLNotFoundException(sprintf('Product with code "%s" was not found!', $item->code));
                    }
                }

                if ($item->productId) {
                    $productKey = $item->productId.($item->variationId ? '/'.$item->variationId : '');
                    if (!($product = ($products[$productKey] ?? null))) {
                        throw new GraphQLNotFoundException(sprintf('Product with ID "%s" and variation ID "%s" was not found!', $item->productId, $item->variationId));
                    }
                }

                if (!$product) {
                    throw new GraphQLNotFoundException('Product was not found!');
                }

                // pokud specifikovali cenu, tak pro ceneni pouziju externi cneu
                if ($item->price) {
                    $itemPrice = new Price(toDecimal($item->price)->removeVat($product->vat), $currency, $product->vat);
                } else {
                    // ve vychozim stavu vezmu cenu od produktu
                    $itemPrice = PriceCalculator::toPrice($product->getProductPrice());
                }

                // vytvorim a pridam novy ProductPurchaseItem
                $productPurchaseItem = new ProductPurchaseItem(
                    $product->id,
                    $product->variationId ?? null,
                    $item->pieces,
                    $itemPrice
                );
                $productPurchaseItem->setProduct($product);

                $purchaseState->addProduct($productPurchaseItem);
            }

            // pridam kupony
            foreach ($input->coupons ?? [] as $coupon) {
                $purchaseState->addCoupon($coupon);
            }

            // prepocitam PurchaseState, abych v nem mel slevy a nasetuju ho do discount manager
            $this->discountManager->setPurchaseState(
                $this->purchaseUtil->recalculateTotalPrices($purchaseState)
            );

            // pres discount manager prepocitam purchase state, aby se mi aplikovali ruzne slevy atd
            return $this->discountManager->recalculate();
        });

        return new \KupShop\GraphQLBundle\ApiAdmin\Types\Purchase\PurchaseState(
            $purchaseState
        );
    }

    protected function createPurchaseState(): PurchaseState
    {
        return new PurchaseState([]);
    }
}
