<?php

declare(strict_types=1);

/*
 * This file is part of the Serenity package.
 *
 * (c) CP Creation <web@cpcreation.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CpCreation\VitiCore\Bridge\Symfony\Bundle\Utils;

use CpCreation\VitiCore\Cart\Model\Cart;
use CpCreation\VitiCore\Cart\Model\CartItem;
use CpCreation\VitiCore\Product\DTO\DegressiveAllocation;
use CpCreation\VitiCore\Product\DTO\DegressiveResult;
use CpCreation\VitiCore\Product\Model\ProductDegressive;
use CpCreation\VitiCore\Product\Model\ProductPrice;
use CpCreation\VitiCore\Product\Model\ProductType;
use CpCreation\VitiCore\Product\Repository\ProductDegressiveRepository;
use CpCreation\VitiCore\Product\Repository\ProductTypeRepository;
use CpCreation\VitiCore\Shop\Repository\ShopConfigRepository;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Session\SessionInterface;

class CalculDegressive
{
    public function __construct(
        private ProductDegressiveRepository $productDegressiveRepository,
        private ProductTypeRepository $productTypeRepository,
        private RequestStack $requestStack,
        private ShopConfigRepository $shopConfigRepository,
    ) {
    }

    private function getSession(): ?SessionInterface
    {
        return $this->requestStack->getSession();
    }

    /**
     * Retourne la dégressivité TTC + sa ventilation par ligne.
     *
     * @throws \Doctrine\ORM\NonUniqueResultException
     * @throws \Exception
     */
    public function calcul(Cart $cart): ?DegressiveResult
    {
        $priceGroupId = $this->getSession()?->get('groupPrice');
        $shopConfig = $this->shopConfigRepository->findOne();

        if (null === $shopConfig || null === $priceGroupId) {
            return null;
        }

        $items = $cart->getItems();
        $equivalencyQty = 0;

        /** @var CartItem $item */
        foreach ($items as $item) {
            $equivalencyQty += $item->getVariant()->getType()->getEquivalencyDegressive() * $item->getQuantity();
        }

        if ($equivalencyQty <= 0) {
            return null;
        }

        if ($shopConfig->isDegrissivityByItem()) {
            $totalAmountTTC = 0;

            /** @var list<DegressiveAllocation> $allocations */
            $allocations = [];

            /** @var CartItem $item */
            foreach ($items as $item) {
                $rule = $this->productDegressiveRepository->findByQuantityAndByItemAndByGroup(
                    $equivalencyQty,
                    $item,
                    $priceGroupId
                );

                if (!$rule) {
                    continue;
                }

                $lineAmountTTC = $this->computeDegressiveAmountForSingleItem($item, $rule, $priceGroupId);

                if ($lineAmountTTC <= 0) {
                    continue;
                }

                $lineAmountTTC = min($lineAmountTTC, $item->getProductTotalFinalTTC());

                $allocations[] = new DegressiveAllocation($item, $lineAmountTTC);
                $totalAmountTTC += $lineAmountTTC;
            }

            return $totalAmountTTC > 0
                ? new DegressiveResult($totalAmountTTC, $allocations)
                : null;
        }

        $typeDefault = $this->productTypeRepository->findDefaultType();

        if (null === $typeDefault) {
            return null;
        }

        $rule = $this->productDegressiveRepository->findByQuantityAndByTypeAndByGroupAndByEquivalency(
            $equivalencyQty,
            $typeDefault->getId()->toString(),
            $cart,
            $priceGroupId
        );

        if ($rule) {
            $eligibleItems = $this->getEligibleItems($cart, $rule);
            $amountTTC = $this->computeGlobalDegressiveAmount($cart, $rule, $typeDefault, $priceGroupId);

            if ($amountTTC <= 0 || [] === $eligibleItems) {
                return null;
            }

            $allocations = $this->allocateAmountOnItems($eligibleItems, $amountTTC);

            return [] !== $allocations
                ? new DegressiveResult($amountTTC, $allocations)
                : null;
        }

        $rules = $this->productDegressiveRepository->findAll();

        /** @var array<string, int> $allAllocations */
        $allAllocations = [];

        /** @var ProductDegressive $candidate */
        foreach ($rules as $candidate) {
            $eligibleItems = $this->getEligibleItems($cart, $candidate);

            if ([] === $eligibleItems) {
                continue;
            }

            $quantity = 0.0;
            foreach ($eligibleItems as $item) {
                if ($candidate->isEquivalency()) {
                    $quantity += $item->getQuantity() * ((float) ($item->getVariant()->getType()->getEquivalencyDegressive() ?? 0.0));
                    continue;
                }

                $quantity += $item->getQuantity();
            }

            if ($quantity <= 0) {
                continue;
            }

            $resolvedRule = $this->productDegressiveRepository->findOneByTypesAndProductsAndQuantity(
                $candidate->getTypes(),
                $candidate->getProducts(),
                $quantity,
                $priceGroupId
            );

            if (!$resolvedRule) {
                continue;
            }

            $amountTTC = $this->computeGlobalDegressiveAmountFromEligibleItems(
                $eligibleItems,
                $resolvedRule,
                $typeDefault,
                $priceGroupId,
                $quantity
            );

            if ($amountTTC <= 0) {
                continue;
            }

            foreach ($this->allocateAmountOnItems($eligibleItems, $amountTTC) as $allocation) {
                $itemId = $allocation->item->getId()->toString();

                if (!isset($allAllocations[$itemId])) {
                    $allAllocations[$itemId] = 0;
                }

                $allAllocations[$itemId] += $allocation->amountTTC;
                $allAllocations[$itemId] = min($allAllocations[$itemId], $allocation->item->getProductTotalFinalTTC());
            }
        }

        if ([] === $allAllocations) {
            return null;
        }

        /** @var list<DegressiveAllocation> $normalizedAllocations */
        $normalizedAllocations = [];

        /** @var CartItem $item */
        foreach ($items as $item) {
            $itemId = $item->getId()->toString();

            if (!isset($allAllocations[$itemId]) || $allAllocations[$itemId] <= 0) {
                continue;
            }

            $normalizedAllocations[] = new DegressiveAllocation($item, $allAllocations[$itemId]);
        }

        if ([] === $normalizedAllocations) {
            return null;
        }

        return new DegressiveResult(
            array_sum(array_map(static fn (DegressiveAllocation $a): int => $a->amountTTC, $normalizedAllocations)),
            $normalizedAllocations
        );
    }

    /**
     * @return list<CartItem>
     */
    private function getEligibleItems(Cart $cart, ProductDegressive $degressive): array
    {
        $eligibleItems = [];

        foreach ($cart->getItems() as $item) {
            $matchesProduct = 0 === $degressive->getProducts()->count()
                || $degressive->getProducts()->contains($item->getVariant()->getProduct());

            $matchesType = $degressive->isEquivalency()
                || 0 === $degressive->getTypes()->count()
                || $degressive->getTypes()->contains($item->getVariant()->getType());

            if ($matchesProduct && $matchesType) {
                $eligibleItems[] = $item;
            }
        }

        return $eligibleItems;
    }

    /**
     * @param list<CartItem> $items
     *
     * @return list<DegressiveAllocation>
     */
    private function allocateAmountOnItems(array $items, int $amountTTC): array
    {
        $baseTTC = 0;

        foreach ($items as $item) {
            $baseTTC += $item->getProductTotalFinalTTC();
        }

        if ($amountTTC <= 0 || $baseTTC <= 0) {
            return [];
        }

        /** @var array<int, DegressiveAllocation> $allocations */
        $allocations = [];
        $allocated = 0;
        $lastIndex = null;

        foreach ($items as $index => $item) {
            $lastIndex = $index;

            $lineBaseTTC = $item->getProductTotalFinalTTC();
            $lineAmount = $this->roundMoney($amountTTC * ($lineBaseTTC / $baseTTC));
            $lineAmount = min($lineAmount, $lineBaseTTC);

            $allocations[$index] = new DegressiveAllocation($item, $lineAmount);
            $allocated += $lineAmount;
        }

        $delta = $amountTTC - $allocated;

        if (null !== $lastIndex && 0 !== $delta) {
            $current = $allocations[$lastIndex];
            $corrected = max(0, min($current->amountTTC + $delta, $current->item->getProductTotalFinalTTC()));

            $allocations[$lastIndex] = new DegressiveAllocation($current->item, $corrected);
        }

        return array_values($allocations);
    }

    private function computeDegressiveAmountForSingleItem(
        CartItem $item,
        ProductDegressive $rule,
        mixed $priceGroupId,
    ): int {
        $amount = 0;
        $lineBaseAmount = $this->getCartItemPriceByGroupId($item, $priceGroupId) * $item->getQuantity();

        if ($rule->getBottlePriceTTC() || $rule->getPackagePriceTTC()) {
            if ($rule->getBottlePriceTTC()) {
                $amount += $rule->getBottlePriceTTC() * $item->getQuantity();
            } else {
                $amount += $rule->getPackagePriceTTC();
            }
        }

        if ($rule->getPackagePercent()) {
            $amount += $this->roundMoney($lineBaseAmount * $rule->getPackagePercent());
        }

        return min($amount, $item->getProductTotalFinalTTC());
    }

    private function computeGlobalDegressiveAmount(
        Cart $cart,
        ProductDegressive $rule,
        ProductType $typeDefault,
        mixed $priceGroupId,
    ): int {
        $eligibleItems = $this->getEligibleItems($cart, $rule);

        if ([] === $eligibleItems) {
            return 0;
        }

        $quantity = 0.0;

        foreach ($eligibleItems as $item) {
            if ($rule->isEquivalency()) {
                $quantity += $item->getQuantity() * ((float) ($item->getVariant()->getType()->getEquivalencyDegressive() ?? 0.0));
                continue;
            }

            $quantity += $item->getQuantity();
        }

        return $this->computeGlobalDegressiveAmountFromEligibleItems(
            $eligibleItems,
            $rule,
            $typeDefault,
            $priceGroupId,
            $quantity
        );
    }

    /**
     * @param list<CartItem> $eligibleItems
     */
    private function computeGlobalDegressiveAmountFromEligibleItems(
        array $eligibleItems,
        ProductDegressive $rule,
        ProductType $typeDefault,
        mixed $priceGroupId,
        float $quantity,
    ): int {
        $amount = 0;
        $totalAmount = 0;
        $allPrices = [];

        foreach ($eligibleItems as $item) {
            $unitPrice = $this->getCartItemPriceByGroupId($item, $priceGroupId);

            if ($unitPrice > 0) {
                $allPrices[] = $unitPrice;
                $totalAmount += $unitPrice * $item->getQuantity();
            }
        }

        if ($rule->getBottlePriceTTC() || $rule->getPackagePriceTTC()) {
            if ($rule->getBottlePriceTTC()) {
                $discountableQuantity = $rule->isAllQuantity()
                    ? max(0.0, $quantity)
                    : max(0.0, $quantity - (float) ($rule->getQtyOf() ?? 0));

                if ($rule->isAllQuantity()) {
                    $amount += $rule->isEquivalency()
                        ? $this->roundMoney($rule->getBottlePriceTTC() * $discountableQuantity)
                        : $rule->getBottlePriceTTC() * (int) $discountableQuantity;
                } else {
                    $amount += $rule->isEquivalency()
                        ? $this->roundMoney($rule->getBottlePriceTTC() * $discountableQuantity)
                        : $rule->getBottlePriceTTC() * (int) $discountableQuantity;
                }
            } else {
                $amount += $rule->getPackagePriceTTC();
            }
        }

        if ($rule->getPackagePercent()) {
            $amount += $this->roundMoney($totalAmount * $rule->getPackagePercent());
        }

        if ($rule->getQtyOfBottleOffered()) {
            $array = $eligibleItems;

            usort(
                $array,
                fn (CartItem $a, CartItem $b): int => $this->sortByQuantityPriceAndType($a, $b, $typeDefault, $priceGroupId)
            );

            $minPriceVariant = [] !== $allPrices ? min($allPrices) : 0;
            $numberBottleOffered = (int) floor($quantity / ((int) $rule->getQtyOf() + 1))
                * (int) $rule->getQtyOfBottleOffered();

            if ($rule->isMostOrderBottleInCart()) {
                if (isset($array[0])) {
                    $amount += $this->getVariantPriceByGroupId($array[0], $priceGroupId) * $numberBottleOffered;
                }
            } else {
                $amount += $minPriceVariant * $numberBottleOffered;
            }
        }

        $eligibleTotalTTC = 0;
        foreach ($eligibleItems as $item) {
            $eligibleTotalTTC += $item->getProductTotalFinalTTC();
        }

        return min($amount, $eligibleTotalTTC);
    }

    public function sortByQuantityPriceAndType(
        CartItem $a,
        CartItem $b,
        ProductType $typeDefault,
        mixed $priceGroupId,
    ): int {
        $aIsTypeDefault = $a->getVariant()->getType()->getName() === $typeDefault->getName();
        $bIsTypeDefault = $b->getVariant()->getType()->getName() === $typeDefault->getName();

        if ($aIsTypeDefault && !$bIsTypeDefault) {
            return -1;
        }

        if (!$aIsTypeDefault && $bIsTypeDefault) {
            return 1;
        }

        if ($aIsTypeDefault && $bIsTypeDefault) {
            $aEquivQuantity = $a->getQuantity() * $a->getVariant()->getType()->getEquivalencyFreeShipping();
            $bEquivQuantity = $b->getQuantity() * $b->getVariant()->getType()->getEquivalencyFreeShipping();

            if ($aEquivQuantity > $bEquivQuantity) {
                return -1;
            }

            if ($aEquivQuantity < $bEquivQuantity) {
                return 1;
            }

            return $this->getVariantPriceByGroupId($a, $priceGroupId) <=> $this->getVariantPriceByGroupId($b, $priceGroupId);
        }

        return 0;
    }

    private function getCartItemPriceByGroupId(CartItem $item, mixed $priceGroupId): int
    {
        /** @var ProductPrice $price */
        foreach ($item->getVariant()->getPrices() as $price) {
            if ($price->getGroup()->getId()->toString() !== $priceGroupId) {
                continue;
            }

            if ($price->getPriceDiscount()) {
                return (int) $price->getPriceDiscount();
            }

            return (int) $price->getPrice();
        }

        return 0;
    }

    private function getVariantPriceByGroupId(CartItem $item, mixed $priceGroupId): int
    {
        $price = $item->getVariant()->getPriceByGroupId($priceGroupId);

        if (null === $price) {
            return 0;
        }

        if ($price->getPriceDiscount()) {
            return (int) $price->getPriceDiscount();
        }

        return (int) $price->getPrice();
    }

    private function roundMoney(float $value): int
    {
        return (int) round($value, 0, \PHP_ROUND_HALF_UP);
    }
}
