<?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\Product;

use CpCreation\VitiCore\Cart\Model\Cart;
use CpCreation\VitiCore\Cart\Model\CartItem;
use CpCreation\VitiCore\Product\DTO\DegressiveAllocation;
use CpCreation\VitiCore\Product\DTO\DegressiveContext;
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\DependencyInjection\Attribute\AsAlias;

#[AsAlias(id: DegressiveCalculatorInterface::class)]
final class BaseDegressiveCalculator implements DegressiveCalculatorInterface
{
    public function __construct(
        private ProductDegressiveRepository $productDegressiveRepository,
        private ProductTypeRepository $productTypeRepository,
        private ShopConfigRepository $shopConfigRepository,
    ) {
    }

    /**
     * Retourne la dégressivité TTC + sa ventilation par ligne.
     *
     * @throws \Doctrine\ORM\NonUniqueResultException
     * @throws \Exception
     */
    public function calculate(DegressiveContext $context): ?DegressiveResult
    {
        $shopConfig = $this->shopConfigRepository->findOne();

        if (null === $shopConfig || $context->equivalencyQty <= 0) {
            return null;
        }

        if ($shopConfig->isDegrissivityByItem()) {
            return $this->calculateByItem($context);
        }

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

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

        return $this->calculateGlobally($context, $typeDefault);
    }

    private function calculateByItem(DegressiveContext $context): ?DegressiveResult
    {
        /** @var list<DegressiveAllocation> $allocations */
        $allocations = [];
        $isBottleOffered = true;
        $hasAllocations = false;

        foreach ($context->cart->getItems() as $item) {
            if (null === $item->getVariant()) {
                continue;
            }

            $rule = $this->productDegressiveRepository->findByQuantityAndByItemAndByGroup(
                $context->equivalencyQty,
                $item,
                $context->priceGroupId,
            );

            if (!$rule) {
                continue;
            }

            $allocation = $this->buildSingleItemAllocation($item, $rule, $context->priceGroupId);

            if (!$allocation instanceof DegressiveAllocation || $allocation->amountTTC <= 0) {
                continue;
            }

            $hasAllocations = true;
            $isBottleOffered = $isBottleOffered && $this->isBottleOfferedRule($rule);
            $allocations[] = $allocation;
        }

        return $this->buildResultFromAllocations($allocations, $hasAllocations && $isBottleOffered);
    }

    private function calculateGlobally(DegressiveContext $context, ProductType $typeDefault): ?DegressiveResult
    {
        $directRule = $this->productDegressiveRepository->findByQuantityAndByTypeAndByGroupAndByEquivalency(
            $context->equivalencyQty,
            $typeDefault->getId()->toString(),
            $context->cart,
            $context->priceGroupId,
        );

        if ($directRule) {
            return $this->calculateFromRule($context->cart, $directRule, $typeDefault, $context->priceGroupId);
        }

        return $this->calculateFromCandidateRules($context->cart, $typeDefault, $context->priceGroupId);
    }

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

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

        $quantity = $this->computeEligibleQuantity($eligibleItems, $rule, $cart);
        $allocations = $this->computeGlobalDegressiveAllocations(
            $eligibleItems,
            $rule,
            $typeDefault,
            $priceGroupId,
            $quantity,
        );

        return $this->buildResultFromAllocations($allocations, $this->isBottleOfferedRule($rule));
    }

    private function calculateFromCandidateRules(
        Cart $cart,
        ProductType $typeDefault,
        mixed $priceGroupId,
    ): ?DegressiveResult {
        /** @var array<string, array{item: CartItem, amountTTC: int, unitAmountTTC: ?int}> $allAllocations */
        $allAllocations = [];
        $isBottleOffered = true;
        $hasAllocations = false;

        foreach ($this->productDegressiveRepository->findAll() as $candidate) {
            $eligibleItems = $this->getEligibleItems($cart, $candidate);

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

            $quantity = $this->computeEligibleQuantity($eligibleItems, $candidate, $cart);

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

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

            if (!$resolvedRule) {
                continue;
            }

            $allocations = $this->computeGlobalDegressiveAllocations(
                $eligibleItems,
                $resolvedRule,
                $typeDefault,
                $priceGroupId,
                $quantity,
            );

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

            $hasAllocations = true;
            $isBottleOffered = $isBottleOffered && $this->isBottleOfferedRule($resolvedRule);

            foreach ($allocations as $allocation) {
                $itemId = $allocation->item->getId()->toString();

                if (!isset($allAllocations[$itemId])) {
                    $allAllocations[$itemId] = [
                        'item' => $allocation->item,
                        'amountTTC' => 0,
                        'unitAmountTTC' => $allocation->unitAmountTTC,
                    ];
                } elseif ($allAllocations[$itemId]['unitAmountTTC'] !== $allocation->unitAmountTTC) {
                    $allAllocations[$itemId]['unitAmountTTC'] = null;
                }

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

        return $this->buildResultFromAllocationMap($allAllocations, $hasAllocations && $isBottleOffered);
    }

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

        foreach ($cart->getItems() as $item) {
            $variant = $item->getVariant();

            if (null === $variant) {
                continue;
            }

            $matchesProduct = 0 === $degressive->getProducts()->count()
                || $degressive->getProducts()->contains($variant->getProduct());

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

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

        return $eligibleItems;
    }

    /**
     * @param list<CartItem> $eligibleItems
     */
    private function computeEligibleQuantity(
        array $eligibleItems,
        ProductDegressive $rule,
        Cart $cart,
    ): float {
        $quantity = 0.0;

        foreach ($eligibleItems as $item) {
            $variant = $item->getVariant();

            if (null === $variant) {
                continue;
            }

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

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

        return $quantity;
    }

    /**
     * @param list<CartItem> $items
     *
     * @return list<DegressiveAllocation>
     */
    private function allocateAmountOnItems(array $items, int $amountTTC): array
    {
        $baseTTC = array_sum(array_map(
            static fn (CartItem $item): int => $item->getTotalTTC(),
            $items,
        ));

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

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

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

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

            $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->getTotalTTC()));

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

        return array_values($allocations);
    }

    private function buildSingleItemAllocation(
        CartItem $item,
        ProductDegressive $rule,
        mixed $priceGroupId,
    ): ?DegressiveAllocation {
        $amount = 0;
        $unitAmountTTC = null;
        $lineBaseAmount = $this->getCartItemPriceByGroupId($item, $priceGroupId) * $item->getQuantity();
        $type = $item->getVariant()?->getType();

        if ($rule->getBottlePriceTTC() || $rule->getPackagePriceTTC()) {
            if ($rule->getBottlePriceTTC()) {
                $discountableUnits = $rule->isEquivalency()
                    ? $item->getQuantity() * ((float) ($type?->getEquivalencyDegressive() ?? 0.0))
                    : (float) $item->getQuantity();

                $amount += $rule->isEquivalency()
                    ? $this->roundMoney($discountableUnits * $rule->getBottlePriceTTC())
                    : $rule->getBottlePriceTTC() * (int) $discountableUnits;
                $unitAmountTTC = $rule->isEquivalency() ? null : $rule->getBottlePriceTTC();
            } else {
                $amount += $rule->getPackagePriceTTC();
            }
        }

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

        $amount = min($amount, $item->getTotalTTC());

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

        return new DegressiveAllocation($item, $amount, $unitAmountTTC);
    }

    /**
     * @param list<CartItem> $eligibleItems
     */
    private function computeGlobalDegressiveAllocations(
        array $eligibleItems,
        ProductDegressive $rule,
        ProductType $typeDefault,
        mixed $priceGroupId,
        float $quantity,
    ): array {
        /** @var array<string, array{item: CartItem, amountTTC: int, unitAmountTTC: ?int}> $allocationMap */
        $allocationMap = [];
        $normalizedQuantity = $this->normalizeQuantityForDegressive($quantity);

        if (null !== $rule->getBottlePriceTTC()) {
            foreach ($this->buildBottlePriceAllocations($eligibleItems, $rule) as $allocation) {
                $this->addAllocation($allocationMap, $allocation);
            }
        } elseif (null !== $rule->getPackagePriceTTC()) {
            foreach ($this->allocateAmountOnItems($eligibleItems, $rule->getPackagePriceTTC()) as $allocation) {
                $this->addAllocation($allocationMap, $allocation);
            }
        }

        if ($rule->getPackagePercent()) {
            foreach ($eligibleItems as $item) {
                $lineBaseAmount = $this->getCartItemPriceByGroupId($item, $priceGroupId) * $item->getQuantity();
                $lineAmount = $this->roundMoney($lineBaseAmount * $rule->getPackagePercent());

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

                $this->addAllocation($allocationMap, new DegressiveAllocation(
                    $item,
                    min($lineAmount, $item->getTotalTTC()),
                ));
            }
        }

        if ($rule->getQtyOfBottleOffered()) {
            foreach ($this->buildOfferedBottleAllocations(
                $eligibleItems,
                $rule,
                $typeDefault,
                $priceGroupId,
                $normalizedQuantity,
            ) as $allocation) {
                $this->addAllocation($allocationMap, $allocation);
            }
        }

        return $this->buildAllocationsFromMap($allocationMap);
    }

    /**
     * @param list<CartItem> $eligibleItems
     *
     * @return list<DegressiveAllocation>
     */
    private function buildBottlePriceAllocations(array $eligibleItems, ProductDegressive $rule): array
    {
        $bottlePriceTTC = $rule->getBottlePriceTTC();

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

        $allocations = [];
        $remainingThresholdUnits = $rule->isAllQuantity() ? 0.0 : max(0.0, (float) ($rule->getQtyOf() ?? 0));

        foreach ($eligibleItems as $item) {
            $variant = $item->getVariant();
            $type = $variant?->getType();

            if (null === $type) {
                continue;
            }

            $lineEquivalentQty = $rule->isEquivalency()
                ? $item->getQuantity() * ((float) ($type->getEquivalencyDegressive() ?? 0.0))
                : (float) $item->getQuantity();

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

            $discountableUnits = $lineEquivalentQty;

            if ($remainingThresholdUnits > 0) {
                $thresholdConsumed = min($remainingThresholdUnits, $discountableUnits);
                $remainingThresholdUnits -= $thresholdConsumed;
                $discountableUnits -= $thresholdConsumed;
            }

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

            $lineAmount = $rule->isEquivalency()
                ? $this->roundMoney($discountableUnits * $bottlePriceTTC)
                : (int) $discountableUnits * $bottlePriceTTC;

            $lineAmount = min($lineAmount, $item->getTotalTTC());

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

            $allocations[] = new DegressiveAllocation(
                $item,
                $lineAmount,
                $rule->isEquivalency() ? null : $bottlePriceTTC,
            );
        }

        return $allocations;
    }

    /**
     * @param list<CartItem> $eligibleItems
     *
     * @return list<DegressiveAllocation>
     */
    private function buildOfferedBottleAllocations(
        array $eligibleItems,
        ProductDegressive $rule,
        ProductType $typeDefault,
        mixed $priceGroupId,
        int $normalizedQuantity,
    ): array {
        $bundleSize = (int) $rule->getQtyOf() + 1;

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

        $numberBottleOffered = (int) floor($normalizedQuantity / $bundleSize)
            * (int) $rule->getQtyOfBottleOffered();

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

        $candidateItems = $this->filterPreferredBottleItems($eligibleItems, $typeDefault);
        $targetItem = $rule->isMostOrderBottleInCart()
            ? $this->findMostOrderedBottleItem($candidateItems, $priceGroupId)
            : $this->findCheapestBottleItem($candidateItems, $priceGroupId);

        if (!$targetItem instanceof CartItem) {
            return [];
        }

        $unitAmountTTC = $this->getVariantPriceByGroupId($targetItem, $priceGroupId);

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

        $amountTTC = min(
            $unitAmountTTC * $numberBottleOffered,
            max(0, $targetItem->getTotalFinalTTC()),
        );

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

        return [
            new DegressiveAllocation($targetItem, $amountTTC, $unitAmountTTC),
        ];
    }

    /**
     * @param array<string, int> $allocationMap
     */
    private function buildResultFromAllocationMap(array $allocationMap, bool $isBottleOffered = false): ?DegressiveResult
    {
        return $this->buildResultFromAllocations($this->buildAllocationsFromMap($allocationMap), $isBottleOffered);
    }

    /**
     * @param array<string, array{item: CartItem, amountTTC: int, unitAmountTTC: ?int}> $allocationMap
     *
     * @return list<DegressiveAllocation>
     */
    private function buildAllocationsFromMap(array $allocationMap): array
    {
        $allocations = [];

        foreach ($allocationMap as $entry) {
            $amountTTC = min(max(0, $entry['amountTTC']), $entry['item']->getTotalTTC());

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

            $allocations[] = new DegressiveAllocation(
                $entry['item'],
                $amountTTC,
                $entry['unitAmountTTC'],
            );
        }

        return $allocations;
    }

    /**
     * @param list<DegressiveAllocation> $allocations
     */
    private function buildResultFromAllocations(array $allocations, bool $isBottleOffered = false): ?DegressiveResult
    {
        if ([] === $allocations) {
            return null;
        }

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

    private function isBottleOfferedRule(ProductDegressive $rule): bool
    {
        return null !== $rule->getQtyOfBottleOffered() && $rule->getQtyOfBottleOffered() > 0;
    }

    /**
     * @param array<string, array{item: CartItem, amountTTC: int, unitAmountTTC: ?int}> $allocationMap
     */
    private function addAllocation(array &$allocationMap, DegressiveAllocation $allocation): void
    {
        $itemId = $allocation->item->getId()->toString();

        if (!isset($allocationMap[$itemId])) {
            $allocationMap[$itemId] = [
                'item' => $allocation->item,
                'amountTTC' => 0,
                'unitAmountTTC' => $allocation->unitAmountTTC,
            ];
        } elseif ($allocationMap[$itemId]['unitAmountTTC'] !== $allocation->unitAmountTTC) {
            $allocationMap[$itemId]['unitAmountTTC'] = null;
        }

        $allocationMap[$itemId]['amountTTC'] += $allocation->amountTTC;
        $allocationMap[$itemId]['amountTTC'] = min($allocationMap[$itemId]['amountTTC'], $allocation->item->getTotalTTC());
    }

    /**
     * @param list<CartItem> $items
     */
    private function computeItemsTotalTTC(array $items): int
    {
        return array_sum(array_map(
            static fn (CartItem $item): int => $item->getTotalTTC(),
            $items,
        ));
    }

    /**
     * @param list<CartItem> $eligibleItems
     *
     * @return list<CartItem>
     */
    private function filterPreferredBottleItems(array $eligibleItems, ProductType $typeDefault): array
    {
        $bottleItems = array_values(array_filter(
            $eligibleItems,
            static fn (CartItem $item): bool => null !== $item->getVariant()?->getType()
                && $item->getVariant()?->getType()?->getId()->equals($typeDefault->getId()),
        ));

        return [] !== $bottleItems ? $bottleItems : $eligibleItems;
    }

    /**
     * @param list<CartItem> $items
     */
    private function findMostOrderedBottleItem(array $items, mixed $priceGroupId): ?CartItem
    {
        if ([] === $items) {
            return null;
        }

        usort($items, function (CartItem $a, CartItem $b) use ($priceGroupId): int {
            if ($a->getQuantity() !== $b->getQuantity()) {
                return $b->getQuantity() <=> $a->getQuantity();
            }

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

        return $items[0] ?? null;
    }

    /**
     * @param list<CartItem> $items
     */
    private function findCheapestBottleItem(array $items, mixed $priceGroupId): ?CartItem
    {
        if ([] === $items) {
            return null;
        }

        usort($items, function (CartItem $a, CartItem $b) use ($priceGroupId): int {
            $priceComparison = $this->getVariantPriceByGroupId($a, $priceGroupId) <=> $this->getVariantPriceByGroupId($b, $priceGroupId);

            if (0 !== $priceComparison) {
                return $priceComparison;
            }

            return $b->getQuantity() <=> $a->getQuantity();
        });

        return $items[0] ?? null;
    }

    public function sortByQuantityPriceAndType(
        CartItem $a,
        CartItem $b,
        ProductType $typeDefault,
        mixed $priceGroupId,
    ): int {
        $aType = $a->getVariant()?->getType();
        $bType = $b->getVariant()?->getType();

        if (null === $aType || null === $bType) {
            return 0;
        }

        $aIsTypeDefault = $aType->getId()->equals($typeDefault->getId());
        $bIsTypeDefault = $bType->getId()->equals($typeDefault->getId());

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

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

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

        $aEquivQuantity = $a->getQuantity() * ((float) ($aType->getEquivalencyFreeShipping() ?? 0.0));
        $bEquivQuantity = $b->getQuantity() * ((float) ($bType->getEquivalencyFreeShipping() ?? 0.0));

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

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

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

    private function getCartItemPriceByGroupId(CartItem $item, mixed $priceGroupId): int
    {
        $variant = $item->getVariant();

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

        /** @var ProductPrice $price */
        foreach ($variant->getPrices() as $price) {
            if ($price->getGroup()->getId()->toString() !== $priceGroupId) {
                continue;
            }

            return (int) ($price->getPriceDiscount() ?: $price->getPrice());
        }

        return 0;
    }

    private function getVariantPriceByGroupId(CartItem $item, mixed $priceGroupId): int
    {
        return $this->getCartItemPriceByGroupId($item, $priceGroupId);
    }

    private function normalizeQuantityForDegressive(float|int $quantity): int
    {
        return max(0, (int) floor((float) $quantity));
    }

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