<?php

declare(strict_types=1);

/*
 * 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\Bridge\Symfony\Bundle\Form\Site\Order;

use CpCreation\VitiCore\Cart\Model\Cart;
use CpCreation\VitiCore\Product\Model\ProductType;
use CpCreation\VitiCore\Product\Repository\ProductTypeRepository;
use CpCreation\VitiCore\Shop\CartShippingEstimator;
use CpCreation\VitiCore\Shop\DTO\ShippingContext;
use CpCreation\VitiCore\Shop\DTO\ShippingQuote;
use CpCreation\VitiCore\Shop\Model\DeliveryMethod;
use CpCreation\VitiCore\Shop\Model\DeliveryMethodCategory;
use CpCreation\VitiCore\Shop\Model\ShippingCountry;
use CpCreation\VitiCore\Shop\Model\ShippingCounty;
use CpCreation\VitiCore\Shop\Repository\ShippingCountryRepository;
use CpCreation\VitiCore\Shop\Repository\ShippingCountyRepository;
use CpCreation\VitiCore\Shop\ShippingContextFactory;
use CpCreation\VitiCore\Shop\ShippingValidator;
use Doctrine\ORM\NonUniqueResultException;
use Symfony\Contracts\Translation\TranslatorInterface;

final class CheckoutDeliveryMethodViewBuilder
{
    /**
     * @var array<string, array{
     *     baseContext: ?ShippingContext,
     *     addressReady: bool,
     *     country: ?ShippingCountry,
     *     county: ?ShippingCounty,
     *     typeDefault: ?ProductType,
     *     types: list<ProductType>
     * }>
     */
    private array $environmentCache = [];

    public function __construct(
        private readonly ShippingContextFactory $shippingContextFactory,
        private readonly ShippingValidator $shippingValidator,
        private readonly CartShippingEstimator $cartShippingEstimator,
        private readonly ShippingCountryRepository $shippingCountryRepository,
        private readonly ShippingCountyRepository $shippingCountyRepository,
        private readonly ProductTypeRepository $productTypeRepository,
        private readonly TranslatorInterface $translator,
    ) {
    }

    /**
     * @param iterable<DeliveryMethod> $deliveryMethods
     *
     * @return list<array{
     *     id: string,
     *     name: ?string,
     *     min_view: bool,
     *     open: bool,
     *     single: bool,
     *     choices: list<array{
     *         method: DeliveryMethod,
     *         amount: int,
     *         price_label: string,
     *         available: bool,
     *         unavailable_reason: ?string
     *     }>
     * }>
     */
    public function build(Cart $cart, iterable $deliveryMethods, ?DeliveryMethod $selectedMethod = null): array
    {
        if ($cart->getItems()->isEmpty()) {
            return [];
        }

        $resolved = $this->resolveShippingEnvironment($cart);
        $baseContext = $resolved['baseContext'];
        $addressReady = $resolved['addressReady'];
        $country = $resolved['country'];
        $county = $resolved['county'];
        $typeDefault = $resolved['typeDefault'];
        $types = $resolved['types'];
        $selectedId = $selectedMethod?->getId()->toString();

        $groups = [];

        foreach ($deliveryMethods as $method) {
            ['available' => $available, 'reason' => $unavailableReason] = $this->getMethodAvailabilityFromCart($cart, $method);

            $quote = null;
            $usesMethodShippingPrices = null !== $baseContext && $this->usesMethodShippingPrices($method);

            if (!$addressReady) {
                $available = false;
                $unavailableReason = $this->translator->trans('checkout_design.delivery_address_required_short');
            }

            if ($usesMethodShippingPrices && $addressReady) {
                $quote = $this->cartShippingEstimator->estimateForMethod($cart, $method, $baseContext);

                if (null === $quote) {
                    $available = false;
                    $unavailableReason = null;
                } elseif ($available) {
                    $context = $baseContext->withDeliveryMethod($method);

                    try {
                        $this->shippingValidator->validate($context, $country, $county, $typeDefault, $types);
                    } catch (\Throwable $e) {
                        $available = false;
                        $unavailableReason = $this->normalizeReason($e->getMessage());
                    }
                }
            } elseif ($available && null !== $baseContext && $addressReady) {
                $context = $baseContext->withDeliveryMethod($method);

                try {
                    $this->shippingValidator->validate($context, $country, $county, $typeDefault, $types);
                    $quote = $this->cartShippingEstimator->estimateForMethod($cart, $method, $baseContext);
                } catch (\Throwable $e) {
                    $available = false;
                    $unavailableReason = $this->normalizeReason($e->getMessage());
                }
            }

            $quote ??= $this->estimateFixedMethodFromCart($cart, $method);
            $available = $available && null !== $quote;

            $category = $method->getCategory();

            if ($category instanceof DeliveryMethodCategory) {
                $groupKey = $category->getId()->toString();
                $groupName = $category->getName();
                $minView = $category->isMinView();
            } else {
                $groupKey = 'method_'.$method->getId()->toString();
                $groupName = null;
                $minView = false;
            }

            if (!isset($groups[$groupKey])) {
                $groups[$groupKey] = [
                    'id' => $groupKey,
                    'name' => $groupName,
                    'min_view' => $minView,
                    'choices' => [],
                    'open' => false,
                    'single' => false,
                ];
            }

            $groups[$groupKey]['choices'][] = [
                'method' => $method,
                'amount' => $quote?->amountTTC ?? 0,
                'price_label' => $this->resolvePriceLabel($quote, $unavailableReason),
                'available' => $available,
                'unavailable_reason' => $available ? null : $unavailableReason,
            ];

            if (null !== $selectedId && $selectedId === $method->getId()->toString()) {
                $groups[$groupKey]['open'] = true;
            }
        }

        foreach ($groups as &$group) {
            $group['single'] = 1 === \count($group['choices']);
        }
        unset($group);

        return array_values($groups);
    }

    /**
     * @return array{
     *     amount: int,
     *     price_label: string,
     *     available: bool,
     *     unavailable_reason: ?string,
     *     selected: bool
     * }|null
     */
    public function buildStandardChoice(Cart $cart, bool $selected = false): ?array
    {
        if ($cart->getItems()->isEmpty()) {
            return null;
        }

        $resolved = $this->resolveShippingEnvironment($cart);
        $baseContext = $resolved['baseContext'];
        $addressReady = $resolved['addressReady'];
        $quote = null;
        $unavailableReason = $addressReady ? null : $this->translator->trans('checkout_design.delivery_address_required_short');

        if ($baseContext instanceof ShippingContext && $addressReady) {
            $quote = $this->cartShippingEstimator->estimateStandard($cart, $baseContext);

            try {
                $standardContext = $baseContext->withDeliveryMethod(null);

                $this->shippingValidator->validate(
                    $standardContext,
                    $resolved['country'],
                    $resolved['county'],
                    $resolved['typeDefault'],
                    $resolved['types'],
                );
            } catch (\Throwable $e) {
                $unavailableReason = $this->normalizeReason($e->getMessage());
            }
        }

        if (null === $quote) {
            return [
                'amount' => 0,
                'price_label' => $this->resolvePriceLabel($quote, $unavailableReason),
                'available' => false,
                'unavailable_reason' => $unavailableReason,
                'selected' => $selected,
            ];
        }

        return [
            'amount' => $quote->amountTTC,
            'price_label' => $this->resolvePriceLabel($quote, $unavailableReason),
            'available' => null === $unavailableReason,
            'unavailable_reason' => $unavailableReason,
            'selected' => $selected,
        ];
    }

    /**
     * @return array{available: bool, reason: ?string}
     */
    private function getMethodAvailabilityFromCart(Cart $cart, DeliveryMethod $method): array
    {
        $reasons = [];
        $minimumEquivalence = $method->getQtyAt();
        $cartEquivalence = $this->normalizeQuantityForValidation($this->countCartEquivalence($cart));

        if (null !== $minimumEquivalence && $minimumEquivalence > $cartEquivalence) {
            $reasons[] = $this->translator->trans(
                'checkout_design.minimum_required_equivalence',
                [
                    '%minimum%' => $this->formatQuantity($minimumEquivalence),
                    '%current%' => $this->formatQuantity($cartEquivalence),
                ],
            );
        }

        if ($this->usesMethodShippingPrices($method)) {
            return [
                'available' => [] === $reasons,
                'reason' => [] === $reasons ? null : implode(' ', $reasons),
            ];
        }

        if (!$method->isMultipleVariant()) {
            return [
                'available' => [] === $reasons,
                'reason' => [] === $reasons ? null : implode(' ', $reasons),
            ];
        }

        $multipleReasons = [];

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

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

            $multiple = $variant->getType()->getMultipleQuantityDepot();

            if ($multiple > 0 && ($item->getQuantity() % $multiple) > 0) {
                $multipleReasons[] = $this->translator->trans(
                    'checkout_design.variant_multiple_required',
                    [
                        '%type%' => $variant->getType()->getName(),
                        '%multiple%' => $multiple,
                        '%quantity%' => $item->getQuantity(),
                    ],
                );
            }
        }

        if ([] !== $multipleReasons) {
            $reasons[] = $this->translator->trans(
                'checkout_design.multiples_required',
                ['%details%' => implode(' ', $multipleReasons)],
            );
        }

        return [
            'available' => [] === $reasons,
            'reason' => [] === $reasons ? null : implode(' ', $reasons),
        ];
    }

    private function estimateFixedMethodFromCart(Cart $cart, DeliveryMethod $method): ?ShippingQuote
    {
        if ($method->isEnabledShippingPrice()) {
            return null;
        }

        if ($method->isDepot()) {
            return ShippingQuote::fromTTC(
                amountTTC: 0,
                taxRate: 0.0,
                shippingName: null,
                details: ['delivery_method_depot'],
            );
        }

        if (null !== $method->getPackagePrice()) {
            return ShippingQuote::fromTTC(
                amountTTC: max(0, $method->getPackagePrice()),
                taxRate: 0.0,
                shippingName: null,
                details: ['delivery_method_package_preview'],
            );
        }

        if (null === $method->getBottlePrice() || $method->getBottlePrice() <= 0) {
            return null;
        }

        $quantity = $method->isDisabledEquivalence()
            ? $this->countCartBottles($cart)
            : $this->countCartEquivalence($cart);

        return ShippingQuote::fromTTC(
            amountTTC: (int) round(max(0.0, $quantity) * $method->getBottlePrice()),
            taxRate: 0.0,
            shippingName: null,
            details: ['delivery_method_bottle_preview'],
        );
    }

    private function resolvePriceLabel(?ShippingQuote $quote, ?string $unavailableReason): string
    {
        if (null !== $unavailableReason && '' !== mb_trim($unavailableReason)) {
            return $this->translator->trans('checkout_design.to_define');
        }

        if ($quote instanceof ShippingQuote) {
            return $this->formatPriceLabel($quote->amountTTC);
        }

        return $this->translator->trans('checkout_design.unavailable');
    }

    /**
     * @return array{
     *     baseContext: ?ShippingContext,
     *     addressReady: bool,
     *     country: ?ShippingCountry,
     *     county: ?ShippingCounty,
     *     typeDefault: ?ProductType,
     *     types: list<ProductType>
     * }
     */
    private function resolveShippingEnvironment(Cart $cart): array
    {
        $cacheKey = $cart->getId()->toString();

        if (isset($this->environmentCache[$cacheKey])) {
            return $this->environmentCache[$cacheKey];
        }

        $baseContext = null;
        $addressReady = $this->isDeliveryAddressReady($cart);

        try {
            $baseContext = $this->shippingContextFactory->createFromCart($cart);
        } catch (\Throwable) {
        }

        $country = null;
        $county = null;
        $typeDefault = null;
        $types = $this->resolveCartTypes($cart);

        if ($baseContext instanceof ShippingContext) {
            $country = $this->resolveCountry($baseContext->countryCode, $baseContext->priceGroupId);
            $county = $this->resolveCounty($baseContext->postalCode, $country);
            $typeDefault = $this->productTypeRepository->findDefaultType();
        }

        return $this->environmentCache[$cacheKey] = [
            'baseContext' => $baseContext,
            'addressReady' => $addressReady,
            'country' => $country,
            'county' => $county,
            'typeDefault' => $typeDefault,
            'types' => $types,
        ];
    }

    private function resolveCountry(?string $countryCode, string $priceGroupId): ?ShippingCountry
    {
        if (null === $countryCode || '' === $countryCode) {
            return null;
        }

        try {
            return $this->shippingCountryRepository->findOneByGroup($countryCode, $priceGroupId);
        } catch (NonUniqueResultException) {
            return null;
        }
    }

    private function resolveCounty(?string $postalCode, ?ShippingCountry $country): ?ShippingCounty
    {
        if (null === $country || !$country->isCounty() || null === $postalCode || '' === $postalCode) {
            return null;
        }

        try {
            return $this->shippingCountyRepository->findDepartmentByCountry(
                mb_substr($postalCode, 0, 2),
                $country->getCode()
            );
        } catch (NonUniqueResultException) {
            return null;
        }
    }

    /**
     * @return list<ProductType>
     */
    private function resolveCartTypes(Cart $cart): array
    {
        $typesBySlug = [];
        $defaultType = $this->productTypeRepository->findDefaultType();

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

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

            $type = $variant->getType();
            $typesBySlug[$type->getSlug()] = $type;
        }

        if ($defaultType instanceof ProductType) {
            $typesBySlug[$defaultType->getSlug()] ??= $defaultType;
        }

        return array_values($typesBySlug);
    }

    private function formatPriceLabel(int $amountTTC): string
    {
        if ($amountTTC <= 0) {
            return $this->translator->trans('checkout_design.free');
        }

        return \sprintf('%s €', number_format($amountTTC / 100, 2, ',', ' '));
    }

    private function countCartBottles(Cart $cart): int
    {
        $quantity = 0;

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

        return $quantity;
    }

    private function countCartEquivalence(Cart $cart): float
    {
        $quantity = 0.0;

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

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

            $type = $variant->getType();

            if ($type->isAccessory()) {
                continue;
            }

            $quantity += max(0, $item->getQuantity()) * ((float) ($type->getEquivalency() ?? 0.0));
        }

        return $quantity;
    }

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

    private function normalizeReason(string $message): ?string
    {
        $message = mb_trim(str_replace('<br/>', ' ', $message));

        return '' === $message ? null : $message;
    }

    private function isDeliveryAddressReady(Cart $cart): bool
    {
        $address = $cart->getDeliveryAddress();

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

        foreach ([
            $address->getFirstname(),
            $address->getLastname(),
            $address->getStreet(),
            $address->getPostal(),
            $address->getCity(),
            $address->getPhone(),
            $address->getCountry(),
        ] as $value) {
            if (null === $value || '' === mb_trim($value)) {
                return false;
            }
        }

        return true;
    }

    private function usesMethodShippingPrices(DeliveryMethod $method): bool
    {
        return $method->isEnabledShippingPrice() && $method->getShippingPrices()->count() > 0;
    }

    private function formatQuantity(int|float $quantity): string
    {
        $quantity = (float) $quantity;

        if ((float) ((int) $quantity) === $quantity) {
            return (string) ((int) $quantity);
        }

        return number_format($quantity, 2, ',', ' ');
    }
}
