<?php

/*
 * This file is part of the VITICORE2 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\Cart;

use CpCreation\VitiCore\Cart\Model\Cart;
use CpCreation\VitiCore\Cart\Model\Promo;
use CpCreation\VitiCore\Cart\Repository\PromoRepository;
use CpCreation\VitiCore\Product\DegressiveManager;
use CpCreation\VitiCore\Product\Model\ProductVariant;
use CpCreation\VitiCore\Shop\Model\ShippingZone;
use CpCreation\VitiCore\Shop\Repository\ShippingCountryRepository;
use CpCreation\VitiCore\Shop\Repository\ShippingCountyRepository;
use CpCreation\VitiCore\Shop\Repository\ShippingZoneRepository;

final class CartPromoHydrator
{
    public function __construct(
        private readonly PromoRepository $promoRepository,
        private readonly ShippingCountryRepository $shippingCountryRepository,
        private readonly ShippingCountyRepository $shippingCountyRepository,
        private readonly ShippingZoneRepository $shippingZoneRepository,
        private readonly PromoEligibilityResolver $promoEligibilityResolver,
        private readonly DegressiveManager $degressiveManager,
    ) {
    }

    public function hydrate(Cart $cart, mixed $groupPrice = null): Cart
    {
        $cartPromos = new CartPromos();
        $zone = $this->resolveZone($cart, $groupPrice);
        $promoCode = $cart->getPromoCodeInput();

        $cart->clearPromoCodeFeedback();

        if (null !== $promoCode && '' !== mb_trim($promoCode)) {
            $eligibility = $this->promoEligibilityResolver->resolveManualCode($cart, $groupPrice);

            if ($eligibility->isApplied() && $eligibility->promo instanceof Promo) {
                $cartPromos->add($this->createFromPromo($cart, $eligibility->promo));
            }

            if (null !== $eligibility->status) {
                $cart->setPromoCodeStatus($eligibility->status);
            }

            if (null !== $eligibility->messageKey) {
                $cart->setPromoCodeMessageKey($eligibility->messageKey);
            }
        }

        foreach ($this->resolveAutomaticPromos($cart, $groupPrice, $zone) as $autoPromo) {
            $cartPromos->add($autoPromo);
        }

        $cart->setCartPromos([] !== $cartPromos->items ? $cartPromos : null);

        return $cart;
    }

    public function createFromPromo(Cart $cart, Promo $promo): CartPromo
    {
        $itemsBaseTTC = $this->getItemsBaseTTC($cart);
        $shippingBaseTTC = max(0, (int) ($cart->getCartShipping()?->amountTTC ?? 0));
        $displayPercent = $promo->getPercent() ?? $promo->getPercentShippingCost();
        $fixedAmountTTC = $this->resolveFixedAmountTTC($cart, $promo);

        $discountItemsTTC = 0;
        $discountShippingTTC = 0;

        if (null !== $promo->getPercent()) {
            $discountItemsTTC += $this->roundMoney($itemsBaseTTC * $promo->getPercent());

            if ($promo->isIncludeShippingFees()) {
                $discountShippingTTC += $this->roundMoney($shippingBaseTTC * $promo->getPercent());
            }
        }

        if (null !== $fixedAmountTTC) {
            [$amountItemsTTC, $amountShippingTTC] = $this->splitFixedAmount(
                amountTTC: $fixedAmountTTC,
                itemsBaseTTC: $itemsBaseTTC,
                shippingBaseTTC: $promo->isIncludeShippingFees() ? $shippingBaseTTC : 0,
            );

            $discountItemsTTC += $amountItemsTTC;
            $discountShippingTTC += $amountShippingTTC;
        }

        if (null !== $promo->getPercentShippingCost()) {
            $discountShippingTTC += $this->roundMoney($shippingBaseTTC * $promo->getPercentShippingCost());
        }

        $discountItemsTTC = min($discountItemsTTC, $itemsBaseTTC);
        $discountShippingTTC = min($discountShippingTTC, $shippingBaseTTC);
        $offeredVariant = $promo->getOfferedVariant();
        $giftPriceTTC = $this->resolveGiftOriginalUnitPriceTTC($offeredVariant);

        return new CartPromo(
            code: $promo->getTitle(),
            title: $promo->getTitle(),
            promoId: $promo->getId()->toString(),
            auto: $promo->isAuto(),
            amountTTC: $fixedAmountTTC,
            percent: $displayPercent,
            onShipping: null !== $promo->getPercentShippingCost(),
            withShippingFees: $promo->isIncludeShippingFees(),
            discountItemsTTC: $discountItemsTTC,
            discountShippingTTC: $discountShippingTTC,
            giftVariantId: $offeredVariant?->getId()->toString(),
            giftProductName: $offeredVariant?->getProduct()?->getFullName(),
            giftTypeName: $offeredVariant?->getType()?->getName(),
            giftUnitLabel: $this->buildUnitLabel($offeredVariant?->getType()?->getVolume(), $offeredVariant?->getType()?->getUnity()),
            giftImagePath: $offeredVariant?->getProduct()?->getSecondImage()?->getPath() ?: $offeredVariant?->getProduct()?->getImage()?->getPath(),
            giftOriginalUnitPriceTTC: $giftPriceTTC,
            giftQuantity: null !== $offeredVariant && null !== $giftPriceTTC && $giftPriceTTC > 0 ? 1 : 0,
        );
    }

    /**
     * @return list<CartPromo>
     */
    private function resolveAutomaticPromos(Cart $cart, mixed $groupPrice, ?ShippingZone $zone): array
    {
        $eligiblePromos = [];

        foreach ($this->promoRepository->findActiveAutoPromosByGroup($cart, $groupPrice, $zone) as $promo) {
            if (!$promo instanceof Promo) {
                continue;
            }

            $eligibility = $this->promoEligibilityResolver->resolvePromo($promo, $cart, $groupPrice, false);

            if (!$eligibility->isApplied()) {
                continue;
            }

            $cartPromo = $this->createFromPromo($cart, $promo);
            $benefitTotalTTC = $cartPromo->getBenefitTotalTTC();

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

            $eligiblePromos[] = [
                'promo' => $promo,
                'cartPromo' => $cartPromo,
                'benefit' => $benefitTotalTTC,
            ];
        }

        if ([] === $eligiblePromos) {
            return [];
        }

        $bestSelection = [];
        $bestBenefit = 0;

        $this->resolveBestAutomaticPromoSelection($eligiblePromos, 0, [], 0, $bestSelection, $bestBenefit);

        return array_map(
            static fn (array $entry): CartPromo => $entry['cartPromo'],
            $bestSelection,
        );
    }

    /**
     * @param list<array{promo: Promo, cartPromo: CartPromo, benefit: int}> $eligiblePromos
     * @param list<array{promo: Promo, cartPromo: CartPromo, benefit: int}> $selectedPromos
     * @param list<array{promo: Promo, cartPromo: CartPromo, benefit: int}> $bestSelection
     */
    private function resolveBestAutomaticPromoSelection(
        array $eligiblePromos,
        int $index,
        array $selectedPromos,
        int $selectedBenefit,
        array &$bestSelection,
        int &$bestBenefit,
    ): void {
        if ($index >= \count($eligiblePromos)) {
            if (
                $selectedBenefit > $bestBenefit
                || ($selectedBenefit === $bestBenefit && \count($selectedPromos) > \count($bestSelection))
            ) {
                $bestBenefit = $selectedBenefit;
                $bestSelection = $selectedPromos;
            }

            return;
        }

        $this->resolveBestAutomaticPromoSelection(
            $eligiblePromos,
            $index + 1,
            $selectedPromos,
            $selectedBenefit,
            $bestSelection,
            $bestBenefit,
        );

        $candidate = $eligiblePromos[$index];

        if (!$this->canCombineAutomaticPromo($candidate, $selectedPromos)) {
            return;
        }

        $selectedPromos[] = $candidate;

        $this->resolveBestAutomaticPromoSelection(
            $eligiblePromos,
            $index + 1,
            $selectedPromos,
            $selectedBenefit + $candidate['benefit'],
            $bestSelection,
            $bestBenefit,
        );
    }

    /**
     * @param array{promo: Promo, cartPromo: CartPromo, benefit: int} $candidate
     * @param list<array{promo: Promo, cartPromo: CartPromo, benefit: int}> $selectedPromos
     */
    private function canCombineAutomaticPromo(array $candidate, array $selectedPromos): bool
    {
        /** @var Promo $candidatePromo */
        $candidatePromo = $candidate['promo'];

        if ([] !== $selectedPromos && !$candidatePromo->isCumulative()) {
            return false;
        }

        foreach ($selectedPromos as $selectedPromoEntry) {
            /** @var Promo $selectedPromo */
            $selectedPromo = $selectedPromoEntry['promo'];

            if (
                !$selectedPromo->isCumulative()
                || $candidatePromo->isIncompatibleWithAutoPromo($selectedPromo)
            ) {
                return false;
            }
        }

        return true;
    }

    private function resolveFixedAmountTTC(Cart $cart, Promo $promo): ?int
    {
        $amountTTC = $promo->getAmountTTC();

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

        if (!$promo->isMultipleByQuantity()) {
            return $amountTTC;
        }

        $minEquivalency = $promo->getMinEquivalency();

        if (null === $minEquivalency || $minEquivalency <= 0) {
            return $amountTTC;
        }

        $quantity = 0 !== $promo->getProducts()->count()
            ? (float) $cart->getQuantityByProducts($promo->getProducts())
            : (float) $cart->getQuantityAllAbsolute();

        $multiplier = (int) floor($quantity / $minEquivalency);

        if ($multiplier <= 0) {
            return 0;
        }

        return $amountTTC * $multiplier;
    }

    public function requiresDeliveryAddress(Promo $promo): bool
    {
        return $promo->getZones()->count() > 0;
    }

    public function hasDeliveryAddressContext(Cart $cart): bool
    {
        $deliveryAddress = $cart->getDeliveryAddress() ?? $cart->getInvoiceAddress();

        if (null === $deliveryAddress) {
            return false;
        }

        if (null === $deliveryAddress->getCountry() || '' === mb_trim($deliveryAddress->getCountry())) {
            return false;
        }

        return null !== $deliveryAddress->getPostal() && '' !== mb_trim($deliveryAddress->getPostal());
    }

    private function resolveZone(Cart $cart, mixed $groupPrice = null): ?ShippingZone
    {
        $deliveryAddress = $cart->getDeliveryAddress() ?? $cart->getInvoiceAddress();

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

        $countryCode = $deliveryAddress->getCountry();
        $postalCode = $deliveryAddress->getPostal();

        if (null === $countryCode || '' === mb_trim($countryCode) || null === $postalCode || '' === mb_trim($postalCode)) {
            return null;
        }

        $country = $this->shippingCountryRepository->findOneByGroup($countryCode, $groupPrice);
        if (null === $country) {
            return null;
        }

        $county = null;
        if ($country->isCounty()) {
            $county = $this->shippingCountyRepository->findDepartmentByCountry(
                mb_substr($postalCode, 0, 2),
                $country->getCode()
            );
        }

        return $this->shippingZoneRepository->findByCountryAndByCounty($country, $county);
    }

    private function getItemsBaseTTC(Cart $cart): int
    {
        $amount = 0;

        foreach ($cart->getItems() as $item) {
            $amount += max(0, $item->getTotalFinalTTC());
        }

        try {
            $amount -= max(0, (int) ($this->degressiveManager->calculateForCart($cart)?->amountTTC ?? 0));
        } catch (\Throwable) {
            // Fallback to the raw item total when degressivity cannot be resolved.
        }

        return max(0, $amount);
    }

    /**
     * @return array{0: int, 1: int}
     */
    private function splitFixedAmount(int $amountTTC, int $itemsBaseTTC, int $shippingBaseTTC): array
    {
        $amountTTC = max(0, $amountTTC);
        $totalBaseTTC = max(0, $itemsBaseTTC + $shippingBaseTTC);

        if (0 === $amountTTC || 0 === $totalBaseTTC) {
            return [0, 0];
        }

        if (0 === $shippingBaseTTC) {
            return [min($amountTTC, $itemsBaseTTC), 0];
        }

        $itemsAmountTTC = $this->roundMoney($amountTTC * ($itemsBaseTTC / $totalBaseTTC));
        $itemsAmountTTC = min($itemsAmountTTC, $itemsBaseTTC);

        $shippingAmountTTC = min($amountTTC - $itemsAmountTTC, $shippingBaseTTC);

        return [$itemsAmountTTC, $shippingAmountTTC];
    }

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

    private function resolveGiftOriginalUnitPriceTTC(?ProductVariant $variant): ?int
    {
        if (null === $variant) {
            return null;
        }

        $price = $variant->getVariantPriceByGroupAvailable();

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

        $priceDiscountTTC = max(0, (int) $price->getPriceDiscount());
        if ($priceDiscountTTC > 0) {
            return $priceDiscountTTC;
        }

        return max(0, (int) $price->getPrice());
    }

    private function buildUnitLabel(?float $volume, ?string $unity): ?string
    {
        if (null === $volume && (null === $unity || '' === mb_trim($unity))) {
            return null;
        }

        $parts = [];

        if (null !== $volume) {
            $parts[] = mb_rtrim(mb_rtrim(number_format($volume, 3, '.', ''), '0'), '.');
        }

        if (null !== $unity && '' !== mb_trim($unity)) {
            $parts[] = mb_trim($unity);
        }

        return '' !== implode(' ', $parts) ? implode(' ', $parts) : null;
    }
}
