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

use CpCreation\VitiCore\Product\Repository\ProductTaxRepository;
use CpCreation\VitiCore\Shop\DTO\ManualShippingResult;
use CpCreation\VitiCore\Shop\DTO\ShippingContext;
use CpCreation\VitiCore\Shop\DTO\ShippingQuote;
use CpCreation\VitiCore\Shop\Model\ShippingConfig;
use CpCreation\VitiCore\Shop\Model\ShippingCountry;
use CpCreation\VitiCore\Shop\Model\ShippingCounty;
use CpCreation\VitiCore\Shop\Repository\ShippingAdditionalManualRepository;
use CpCreation\VitiCore\Shop\Repository\ShippingAutoRepository;
use CpCreation\VitiCore\Shop\Repository\ShippingAutoSupRepository;
use CpCreation\VitiCore\Shop\Repository\ShippingConfigRepository;
use CpCreation\VitiCore\Shop\Repository\ShippingCountryRepository;
use CpCreation\VitiCore\Shop\Repository\ShippingCountyRepository;
use CpCreation\VitiCore\Shop\Repository\ShippingPriceRepository;
use Symfony\Component\DependencyInjection\Attribute\AsAlias;
use Symfony\Contracts\Translation\TranslatorInterface;

#[AsAlias(id: ShippingCalculatorInterface::class)]
final class BaseShippingCalculator implements ShippingCalculatorInterface
{
    public function __construct(
        private ShippingConfigRepository $shippingConfigRepository,
        private ShippingCountryRepository $shippingCountryRepository,
        private ShippingCountyRepository $shippingCountyRepository,
        private ShippingPriceRepository $shippingPriceRepository,
        private ShippingAdditionalManualRepository $shippingAdditionalManualRepository,
        private ShippingAutoRepository $shippingAutoRepository,
        private ShippingAutoSupRepository $shippingAutoSupRepository,
        private ProductTaxRepository $productTaxRepository,
        private TranslatorInterface $translator,
    ) {
    }

    /**
     * @throws \Exception
     */
    public function calculate(ShippingContext $context): ShippingQuote
    {
        $shippingConfig = $this->shippingConfigRepository->findOne();

        if (null === $shippingConfig) {
            throw new \Exception($this->translator->trans('shop.error.calcul'));
        }

        $fixedDeliveryMethodQuote = $this->calculateFixedDeliveryMethodQuote($context, $shippingConfig);

        if (null !== $fixedDeliveryMethodQuote) {
            return $fixedDeliveryMethodQuote;
        }

        if (null === $context->countryCode || '' === $context->countryCode) {
            throw new \Exception($this->translator->trans('shop.error.error_country'));
        }

        $country = $this->shippingCountryRepository->findOneByGroup(
            $context->countryCode,
            $context->priceGroupId
        );

        if (null === $country) {
            throw new \Exception($this->translator->trans('shop.error.error_country'));
        }

        $county = $this->resolveCounty($context, $country);

        $manualResult = $this->calculateManualShipping(
            $context,
            $shippingConfig,
            $country,
            $county,
        );

        $autoAmountTTC = $this->calculateAutoAmountTTC(
            $context,
            $shippingConfig,
            $country,
            $county,
            $manualResult->remainingAutoEquivalence,
        );

        $totalAmountTTC = $manualResult->amountTTC + $autoAmountTTC;
        $shippingTaxRate = $this->resolveShippingTaxRate($shippingConfig);

        return ShippingQuote::fromTTC(
            amountTTC: max(0, $totalAmountTTC),
            taxRate: $shippingTaxRate,
            shippingName: null,
            details: ['base_shipping_manual', 'base_shipping_auto'],
        );
    }

    private function calculateFixedDeliveryMethodQuote(
        ShippingContext $context,
        ShippingConfig $shippingConfig,
    ): ?ShippingQuote {
        $deliveryMethod = $context->deliveryMethod;

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

        if ($deliveryMethod->isEnabledShippingPrice()) {
            return null;
        }

        $shippingTaxRate = $this->resolveShippingTaxRate($shippingConfig);

        if (null !== $deliveryMethod->getPackagePrice() && $deliveryMethod->getPackagePrice() >= 0) {
            return ShippingQuote::fromTTC(
                amountTTC: $deliveryMethod->getPackagePrice(),
                taxRate: $shippingTaxRate,
                shippingName: null,
                details: ['delivery_method_package_price'],
            );
        }

        if (null !== $deliveryMethod->getBottlePrice() && $deliveryMethod->getBottlePrice() > 0) {
            $quantity = $deliveryMethod->isDisabledEquivalence()
                ? (float) $context->equivalenceOff
                : $context->quantityAllAbsolute;

            return ShippingQuote::fromTTC(
                amountTTC: $this->roundMoney((float) $deliveryMethod->getBottlePrice() * max(0.0, $quantity)),
                taxRate: $shippingTaxRate,
                shippingName: null,
                details: ['delivery_method_bottle_price'],
            );
        }

        return null;
    }

    /**
     * @throws \Exception
     */
    private function resolveCounty(
        ShippingContext $context,
        ShippingCountry $country,
    ): ?ShippingCounty {
        if (!$country->isCounty()) {
            return null;
        }

        if (null === $context->postalCode || '' === $context->postalCode) {
            throw new \Exception($this->translator->trans('shop.error.postal_code'));
        }

        $department = mb_substr($context->postalCode, 0, 2);

        $county = $this->shippingCountyRepository->findDepartmentByCountry(
            $department,
            $country->getCode()
        );

        if (null === $county && null === $context->deliveryMethod) {
            throw new \Exception($this->translator->trans('shop.error.empty_shipping'));
        }

        return $county;
    }

    private function resolveShippingTaxRate(ShippingConfig $shippingConfig): float
    {
        if ($shippingConfig->getTvaManu() > 0) {
            return (float) $shippingConfig->getTvaManu();
        }

        $taxDefault = $this->productTaxRepository->findOneBy(['byDefault' => true]);

        if (null !== $taxDefault) {
            return (float) $taxDefault->getValue();
        }

        return 0.0;
    }

    /**
     * Calcule la partie manuelle du transport
     * et retourne aussi le reliquat à envoyer vers l'auto.
     *
     * @throws \Exception
     */
    private function calculateManualShipping(
        ShippingContext $context,
        ShippingConfig $shippingConfig,
        ?ShippingCountry $country,
        ?ShippingCounty $county,
    ): ManualShippingResult {
        if (null === $country) {
            return new ManualShippingResult(0, 0.0);
        }

        $quantities = $context->quantities;
        $quantitiesManual = $context->quantitiesManual;
        $quantitiesAutoFallback = $context->quantitiesAutoFallback;
        $quantitiesMinCart = $context->quantitiesMinCart;

        $remainingAutoEquivalence = 0.0;
        $fdpTypeMargeTotal = 0;
        $fdpTypeMargeWithoutMargeTotal = 0;
        $zoneTest = null;
        $errorTypes = [];
        $i = 0;
        $shippingDeliveryMethod = $this->resolveShippingPriceDeliveryMethod($context);

        $typeDefaultSlug = $context->typeDefaultSlug;
        $quantitiesManual = array_reverse($quantitiesManual, true);

        if (
            $country->isCounty()
            && $context->equivalenceAuto > 0
            && $shippingConfig->isForceEquivalencyForAuto()
        ) {
            $equivalenceAutoShipping = $this->shippingPriceRepository->findByQuantityAndByTypeAndByZoneAndByGroup(
                $this->normalizeQuantityForShippingLookup($context->equivalenceAuto),
                $typeDefaultSlug,
                $county?->getZone(),
                $context->priceGroupId,
                $shippingDeliveryMethod
            );

            if (!$equivalenceAutoShipping) {
                foreach ($quantitiesManual as $type => $quantity) {
                    if (!($context->disabledShippingAuto[$type] ?? false)) {
                        unset($quantitiesManual[$type]);
                    }
                }

                $remainingAutoEquivalence = $context->equivalenceAuto;
            }
        }

        foreach ($quantitiesManual as $type => $quantity) {
            if ($quantity <= 0) {
                ++$i;
                continue;
            }

            $isDisabledManu = $context->disabledShippingManu[$type] ?? false;
            $isDisabledAuto = $context->disabledShippingAuto[$type] ?? false;

            if (false === $isDisabledManu) {
                $shippingPrice = null;
                $shippingQuantity = $this->normalizeQuantityForShippingLookup($quantity);

                if ($country->isCounty()) {
                    $shippingPrice = $this->shippingPriceRepository->findByQuantityAndByTypeAndByZoneAndByGroup(
                        $shippingQuantity,
                        $type,
                        $county?->getZone(),
                        $context->priceGroupId,
                        $shippingDeliveryMethod
                    );

                    if (0 === $i && null !== $county?->getZone()) {
                        $zoneTest = $county->getZone()->getId()->toString();
                    }
                } else {
                    foreach ($country->getZones() as $zone) {
                        $shippingPrice = $this->shippingPriceRepository->findByQuantityAndByTypeAndByZoneAndByGroup(
                            $shippingQuantity,
                            $type,
                            $zone,
                            $context->priceGroupId,
                            $shippingDeliveryMethod
                        );

                        if ($shippingPrice) {
                            if (0 === $i) {
                                $zoneTest = $zone->getId()->toString();
                            }

                            break;
                        }
                    }
                }

                if ($shippingPrice) {
                    $fdpType = 0;

                    $freeShippingWithBottle = $context->freeShippingWithBottle[$type] ?? false;
                    $minFreeShippingWithBottle = $context->minFreeShippingWithBottle[$type] ?? null;
                    $zonesFreeShippingWithBottle = $context->zonesFreeShippingWithBottle[$type] ?? [];

                    if (
                        $freeShippingWithBottle
                        && (null === $minFreeShippingWithBottle || $context->equivalenceDefault >= $minFreeShippingWithBottle)
                        && null !== $zoneTest
                        && \in_array($zoneTest, $zonesFreeShippingWithBottle, true)
                    ) {
                        $fdpType = 0;
                    } else {
                        if ($shippingPrice->getBottlePrice()) {
                            $fdpType = (int) $shippingPrice->getBottlePrice() * $shippingQuantity;
                        } else {
                            $fdpType = (int) $shippingPrice->getPackagePrice();
                        }

                        if ($shippingPrice->getPrivateMargePercent() > 0) {
                            $fdpType += $this->roundMoney($fdpType * (float) $shippingPrice->getPrivateMargePercent());
                        }

                        if ($shippingPrice->getPrivateMarge() > 0) {
                            $fdpType += (int) $shippingPrice->getPrivateMarge();
                        }
                    }

                    if ($shippingPrice->isConfigGlobal()) {
                        $fdpTypeMargeTotal += $fdpType;
                    } else {
                        $fdpTypeMargeWithoutMargeTotal += $fdpType;
                    }

                    unset($quantitiesManual[$type], $quantities[$type]);
                } else {
                    if (null !== $shippingDeliveryMethod) {
                        throw new \Exception($this->translator->trans('shop.error.missing_shipping_delivery_method'));
                    }

                    if (true === $isDisabledAuto) {
                        $errorTypes[] = ($context->typeNames[$type] ?? $type).' x'.$quantity;
                    }

                    $remainingAutoEquivalence += $quantitiesAutoFallback[$type] ?? 0.0;

                    unset($quantitiesManual[$type]);

                    if ($typeDefaultSlug !== $type) {
                        unset($quantities[$type]);
                    }
                }
            } else {
                if (false === $isDisabledAuto) {
                    $remainingAutoEquivalence += $quantitiesAutoFallback[$type] ?? 0.0;
                } else {
                    $errorTypes[] = ($context->typeNames[$type] ?? $type).' x'.$quantity;
                }
            }

            ++$i;
        }

        if ([] !== $errorTypes) {
            throw new \Exception($this->translator->trans('shop.error.error_product_delivery_method_standard', ['%types%' => implode(', ', $errorTypes)]).'<br/>');
        }

        if ($fdpTypeMargeTotal > 0) {
            if ($shippingConfig->getAmountFixedManu() > 0) {
                $fdpTypeMargeTotal += (int) $shippingConfig->getAmountFixedManu();
            }

            if (
                $shippingConfig->getAmountMinVDManu() > 0
                && $context->quantityAllAbsolute >= $shippingConfig->getQtyMinVDManu()
            ) {
                $taxeVD = $this->roundMoney(
                    ($fdpTypeMargeTotal + $context->itemsTotalTTC) * (float) $shippingConfig->getMarginManu()
                );

                if ($taxeVD < $shippingConfig->getAmountMinVDManu()) {
                    $taxeVD = (int) $shippingConfig->getAmountMinVDManu();
                }

                $fdpTypeMargeTotal += $taxeVD;
            }

            if ($shippingConfig->getMarginClientManu() > 0) {
                $fdpTypeMargeTotal += $this->roundMoney(
                    $fdpTypeMargeTotal * (float) $shippingConfig->getMarginClientManu()
                );
            }
        }

        $fdpManu = $fdpTypeMargeTotal + $fdpTypeMargeWithoutMargeTotal;

        if ($shippingConfig->getTvaManu() > 0) {
            $fdpManu = $this->roundMoney(
                $fdpTypeMargeTotal * (1 + (float) $shippingConfig->getTvaManu())
            ) + $fdpTypeMargeWithoutMargeTotal;
        }

        if ($shippingConfig->getAmountByBottleTTC() > 0) {
            $remainingBottles = max(
                0.0,
                $context->quantityAllAbsolute - $remainingAutoEquivalence
            );

            $fdpManu += (int) $shippingConfig->getAmountByBottleTTC()
                * $this->normalizeQuantityForShippingLookup($remainingBottles);
        }

        foreach ($quantitiesMinCart as $key => $quantityByType) {
            foreach ($country->getZones() as $zone) {
                $shippingAdditional = $this->shippingAdditionalManualRepository->findByQuantityAndByTypeAndByZoneAndByGroup(
                    $this->normalizeQuantityForShippingLookup($quantityByType),
                    $key,
                    $zone,
                    $context->priceGroupId
                );

                if (!$shippingAdditional) {
                    continue;
                }

                $fdpShippingAdditional = $shippingAdditional->getPackagePrice()
                    ?: ((int) $shippingAdditional->getBottlePrice() * $this->normalizeQuantityForShippingLookup($quantityByType));

                if ($shippingConfig->getTvaManu() > 0 && $shippingAdditional->isHt()) {
                    $fdpManu += $this->roundMoney(
                        $fdpShippingAdditional * (1 + (float) $shippingConfig->getTvaManu())
                    );
                } else {
                    $fdpManu += (int) $fdpShippingAdditional;
                }

                break;
            }
        }

        return new ManualShippingResult(
            amountTTC: max(0, $fdpManu),
            remainingAutoEquivalence: max(0.0, $remainingAutoEquivalence),
        );
    }

    /**
     * Calcule la partie automatique du transport (TTC) sur le reliquat.
     *
     * @throws \Exception
     */
    private function calculateAutoAmountTTC(
        ShippingContext $context,
        ShippingConfig $shippingConfig,
        ShippingCountry $country,
        ?ShippingCounty $county,
        float $remainingAutoEquivalence,
    ): int {
        if ($remainingAutoEquivalence <= 0) {
            return 0;
        }

        $normalizedEquivalence = $this->normalizeQuantityForShippingLookup($remainingAutoEquivalence);
        $searchByHm = false;

        $shippingSup = $this->shippingAutoSupRepository->findOneBy([
            'country' => $country->getCode(),
            'county' => $county ? $county->getCode() : null,
            'city' => $context->city,
        ]);

        if ($shippingSup) {
            $searchByHm = true;
        }

        if ($country->isCounty()) {
            if (null === $county) {
                return 0;
            }

            $shippingPrice = $this->shippingAutoRepository->findByQuantityAndByCountyAndByCountry(
                $normalizedEquivalence,
                $county->getCode(),
                $country->getCode(),
                $searchByHm
            );

            if (!$shippingPrice) {
                $searchByHm = false;

                $shippingPrice = $this->shippingAutoRepository->findByQuantityAndByCountyAndByCountry(
                    $normalizedEquivalence,
                    $county->getCode(),
                    $country->getCode()
                );
            }
        } else {
            $shippingPrice = $this->shippingAutoRepository->findByQuantityAndByCountry(
                $normalizedEquivalence,
                $country->getCode()
            );
        }

        if (!$shippingPrice) {
            throw new \Exception($this->translator->trans('shop.error.missing_shipping'));
        }

        $fdp = 0;

        if ($shippingPrice->getBottlePrice()) {
            $fdp += (int) $shippingPrice->getBottlePrice() * $normalizedEquivalence;
        } else {
            $fdp += (int) $shippingPrice->getPackagePrice();
        }

        if (
            $shippingConfig->getAmountPercentVDAuto()
            && $normalizedEquivalence >= $shippingConfig->getQtyMinVDAuto()
        ) {
            $taxDefault = $this->productTaxRepository->findOneBy(['byDefault' => true]);

            if ($taxDefault) {
                $fdpVD = round(
                    ($context->itemsTotalTTC / (1 + $taxDefault->getValue()))
                    * $shippingConfig->getAmountPercentVDAuto(),
                    2
                );

                if (
                    $shippingConfig->getAmountMinVDAuto()
                    && $fdpVD < $shippingConfig->getAmountMinVDAuto()
                ) {
                    $fdpVD = $shippingConfig->getAmountMinVDAuto();
                }

                $fdp += (int) ceil($fdpVD);
            }

            if ($shippingConfig->getAmountVDAuto() > 0) {
                $fdp += (int) $shippingConfig->getAmountVDAuto();
            }
        }

        $fdp += (int) $shippingConfig->getAmountFixedBeforeMargeAuto();

        $shippingSup = $this->shippingAutoSupRepository->findOneBy([
            'country' => $country->getCode(),
            'county' => $county ? $county->getCode() : null,
            'city' => $context->city,
        ]);

        if ($shippingSup && !$searchByHm) {
            $fdp += (int) $shippingConfig->getHighMountainAmount();
        }

        $fdp = (int) ceil($fdp + ($fdp * $shippingConfig->getMarginAuto()));
        $fdp = (int) ceil($fdp + ($fdp * $shippingConfig->getMarginGazoleAuto()));
        $fdp = (int) ceil($fdp + ($fdp * $shippingConfig->getMarginClientAuto()));
        $fdp += (int) $shippingConfig->getAmountFixedAuto();

        if ($shippingConfig->getAmountAutoByBottleTTC() > 0) {
            $fdp += (int) $shippingConfig->getAmountAutoByBottleTTC() * $normalizedEquivalence;
        }

        return max(0, $fdp);
    }

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

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

    private function resolveShippingPriceDeliveryMethod(ShippingContext $context): ?\CpCreation\VitiCore\Shop\Model\DeliveryMethod
    {
        $deliveryMethod = $context->deliveryMethod;

        if (null === $deliveryMethod || !$this->usesMethodShippingPrices($deliveryMethod)) {
            return null;
        }

        return $deliveryMethod;
    }

    private function usesMethodShippingPrices(\CpCreation\VitiCore\Shop\Model\DeliveryMethod $deliveryMethod): bool
    {
        return $deliveryMethod->isEnabledShippingPrice() && $deliveryMethod->getShippingPrices()->count() > 0;
    }
}
