<?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\Bridge\Symfony\Bundle\Context\GroupContext;
use CpCreation\VitiCore\Cart\Model\Cart;
use CpCreation\VitiCore\Cart\Model\DeliveryAddress;
use CpCreation\VitiCore\Cart\Model\InvoiceAddress;
use CpCreation\VitiCore\Cart\Repository\CartRepository;
use CpCreation\VitiCore\Product\DegressiveManager;
use CpCreation\VitiCore\Product\Model\ProductType;
use CpCreation\VitiCore\Product\Repository\ProductTaxRepository;
use CpCreation\VitiCore\Product\Repository\ProductTypeRepository;
use CpCreation\VitiCore\Shop\CartShippingEstimator;
use CpCreation\VitiCore\Shop\DTO\ShippingQuote;
use CpCreation\VitiCore\Shop\Model\ShippingCountry;
use CpCreation\VitiCore\Shop\Model\ShippingCounty;
use CpCreation\VitiCore\Shop\Repository\ShopConfigRepository;
use CpCreation\VitiCore\Shop\Repository\ShippingCountryRepository;
use CpCreation\VitiCore\Shop\Repository\ShippingCountyRepository;
use CpCreation\VitiCore\Shop\ShippingContextFactory;
use CpCreation\VitiCore\Shop\ShippingValidator;
use CpCreation\VitiCore\User\Model\User;
use Doctrine\ORM\NonUniqueResultException;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\HttpFoundation\RequestStack;

final class CartService
{
    private bool $cartResolved = false;

    private ?Cart $resolvedCart = null;

    public function __construct(
        private readonly Security $security,
        private readonly RequestStack $requestStack,
        private readonly CartRepository $cartRepository,
        private readonly ProductTaxRepository $productTaxRepository,
        private readonly ProductTypeRepository $productTypeRepository,
        private readonly CartShippingEstimator $cartShippingEstimator,
        private readonly ShippingContextFactory $shippingContextFactory,
        private readonly ShippingValidator $shippingValidator,
        private readonly ShopConfigRepository $shopConfigRepository,
        private readonly ShippingCountryRepository $shippingCountryRepository,
        private readonly ShippingCountyRepository $shippingCountyRepository,
        private readonly CartPromoHydrator $cartPromoHydrator,
        private readonly CartPricingFactory $cartPricingFactory,
        private readonly DegressiveManager $degressiveManager,
    ) {
    }

    public function removeCart(): void
    {
        if ($cart = $this->cartRepository->findOneCartInProgressById($this->requestStack->getSession()->get('cart'), false)) {
            $this->cartRepository->delete($cart);
        }
        $this->requestStack->getSession()->remove('cart');
        $this->cartResolved = true;
        $this->resolvedCart = null;
    }

    public function createCart(): Cart
    {
        $cart = new Cart();
        $cart->setDocumentType(Cart::DOCUMENT_TYPE_ORDER);
        $taxDefault = $this->productTaxRepository->findOneByDefault();
        if (null === $taxDefault || null === $taxDefault->getValue()) {
            throw new \LogicException('No default product tax configured.');
        }
        $cart->setTax((string) $taxDefault->getValue());
        $this->setUser($cart);
        $this->syncPartnerCode($cart);
        $this->requestStack->getSession()->set('cart', $cart->getId());
        $this->cartRepository->save($cart);
        $this->rememberCart($cart);

        return $cart;
    }

    public function claimSessionCartForUser(User $user): ?Cart
    {
        $session = $this->requestStack->getSession();
        $sessionCartId = $session?->get('cart');

        if (!\is_scalar($sessionCartId) && !$sessionCartId instanceof \Stringable) {
            return null;
        }

        $cart = $this->cartRepository->findOneCartInProgressById((string) $sessionCartId);

        if (!$cart instanceof Cart) {
            $session?->remove('cart');
            return null;
        }

        if ($this->isOwnedByAnotherUser($cart, $user)) {
            $session?->remove('cart');

            return null;
        }

        if (!$this->claimCartForUser($cart, $user)) {
            return $cart;
        }

        $this->cartRepository->save($cart);
        $this->rememberCart($cart);

        return $cart;
    }

    public function getCart(): ?Cart
    {
        $session = $this->requestStack->getSession();

        if ($this->cartResolved) {
            return $this->resolvedCart;
        }

        $sessionCartId = $session?->get('cart');

        if ($sessionCartId && $cart = $this->cartRepository->findOneCartInProgressById($sessionCartId)) {
            if ($this->isOwnedByAnotherAuthenticatedUser($cart)) {
                $session?->remove('cart');
            } else {
            $cartClaimed = $this->claimCartForAuthenticatedUser($cart);
            $partnerCodeChanged = $this->syncPartnerCode($cart);
            $this->hydrateShipping($cart);
            $this->hydratePromos($cart);
            $this->hydratePricing($cart);
            if ($partnerCodeChanged || $cartClaimed) {
                $this->cartRepository->save($cart);
            }
            $this->rememberCart($cart);

            return $cart;
            }
        } elseif ($sessionCartId) {
            $session?->remove('cart');
        }

        /** @var User|null $user */
        $user = $this->security->getUser();

        if ($user instanceof User && $cart = $this->cartRepository->findOneCartInProgressByUser($user)) {
            $duplicatesRemoved = $this->cleanupDuplicateUserCarts($user, $cart);
            $session?->set('cart', $cart->getId()->toString());

            $partnerCodeChanged = $this->syncPartnerCode($cart);
            $this->hydrateShipping($cart);
            $this->hydratePromos($cart);
            $this->hydratePricing($cart);
            if ($partnerCodeChanged || $duplicatesRemoved) {
                $this->cartRepository->save($cart);
            }
            $this->rememberCart($cart);

            return $cart;
        }

        $this->cartResolved = true;
        $this->resolvedCart = null;

        return null;
    }

    public function hydrateShipping(Cart $cart): Cart
    {
        if ($cart->isCheckoutPayed()) {
            return $cart;
        }

        $initialShippingState = $this->captureShippingState($cart);
        $this->resetUnavailableDeliveryMethod($cart);

        $quote = $this->resolveCurrentShippingQuote($cart);

        if (null === $quote) {
            $cart->setCartShipping(null);

            if ($this->shippingStateChanged($cart, $initialShippingState)) {
                $this->cartRepository->save($cart);
            }

            $this->rememberCart($cart);

            return $cart;
        }

        $cart->setCartShipping(new CartShipping(
            amountTTC: $quote->amountTTC,
            taxRate: $quote->taxRate,
            name: $quote->shippingName ?? $cart->getDeliveryMethod()?->getName(),
        ));

        if ($this->shippingStateChanged($cart, $initialShippingState)) {
            $this->cartRepository->save($cart);
        }

        $this->rememberCart($cart);

        return $cart;
    }

    private function resolveCurrentShippingQuote(Cart $cart): ?ShippingQuote
    {
        try {
            $context = $this->shippingContextFactory->createFromCart($cart);
        } catch (\Throwable) {
            return null;
        }

        $country = $this->resolveCountry($context->countryCode, $context->priceGroupId);
        $county = $this->resolveCounty($context->postalCode, $country);
        $typeDefault = $this->productTypeRepository->findDefaultType();
        $types = $this->resolveCartTypes($cart);

        try {
            $this->shippingValidator->validate($context, $country, $county, $typeDefault, $types);
        } catch (\Throwable) {
            return null;
        }

        return $this->cartShippingEstimator->estimate($cart);
    }

    /**
     * @return array{
     *     delivery_method_id: ?string,
     *     shipping_amount_ttc: ?int,
     *     shipping_tax_rate: ?float,
     *     shipping_name: ?string
     * }
     */
    private function captureShippingState(Cart $cart): array
    {
        return [
            'delivery_method_id' => $cart->getDeliveryMethod()?->getId()->toString(),
            'shipping_amount_ttc' => $cart->getCartShipping()?->amountTTC,
            'shipping_tax_rate' => $cart->getCartShipping()?->taxRate,
            'shipping_name' => $cart->getCartShipping()?->name,
        ];
    }

    /**
     * @param array{
     *     delivery_method_id: ?string,
     *     shipping_amount_ttc: ?int,
     *     shipping_tax_rate: ?float,
     *     shipping_name: ?string
     * } $initialShippingState
     */
    private function shippingStateChanged(Cart $cart, array $initialShippingState): bool
    {
        return $initialShippingState !== $this->captureShippingState($cart);
    }

    public function hydratePricing(Cart $cart): Cart
    {
        if ($cart->isCheckoutPayed()) {
            return $cart;
        }

        $degressiveResult = $this->degressiveManager->calculateForCart($cart);
        $cart->setPricing($this->cartPricingFactory->createFromCart($cart, $degressiveResult));
        $this->rememberCart($cart);

        return $cart;
    }

    public function hydratePromos(Cart $cart): Cart
    {
        if ($cart->isCheckoutPayed()) {
            return $cart;
        }

        $this->cartPromoHydrator->hydrate($cart, GroupContext::$groupId);
        $this->rememberCart($cart);

        return $cart;
    }

    public function hydrateCart(Cart $cart): Cart
    {
        if ($cart->isCheckoutPayed()) {
            $this->rememberCart($cart);

            return $cart;
        }

        $this->syncPartnerCode($cart);
        $this->hydrateShipping($cart);
        $this->hydratePromos($cart);
        $this->hydratePricing($cart);
        $cart->removeEmptyItems();
        $this->cartRepository->save($cart);
        $this->rememberCart($cart);

        return $cart;
    }

    private function rememberCart(?Cart $cart): void
    {
        $this->cartResolved = true;
        $this->resolvedCart = $cart;
    }

    private function syncPartnerCode(Cart $cart): bool
    {
        $partnerCode = $this->requestStack->getSession()?->get('partnerCode');
        $partnerCode = \is_string($partnerCode) && '' !== $partnerCode
            ? $partnerCode
            : GroupContext::$partnerCode;

        if ($cart->getGroupCode() === $partnerCode) {
            return false;
        }

        $cart->setGroupCode($partnerCode);

        return true;
    }

    private function claimCartForAuthenticatedUser(Cart $cart): bool
    {
        $user = $this->security->getUser();

        if (!$user instanceof User) {
            return false;
        }

        return $this->claimCartForUser($cart, $user);
    }

    private function isOwnedByAnotherAuthenticatedUser(Cart $cart): bool
    {
        $user = $this->security->getUser();

        if (!$user instanceof User) {
            return false;
        }

        return $this->isOwnedByAnotherUser($cart, $user);
    }

    private function isOwnedByAnotherUser(Cart $cart, User $user): bool
    {
        return null !== $cart->getUser() && $cart->getUser() !== $user;
    }

    private function claimCartForUser(Cart $cart, User $user): bool
    {
        if (null !== $cart->getUser() && $cart->getUser() !== $user) {
            return false;
        }

        $duplicatesRemoved = $this->cleanupDuplicateUserCarts($user, $cart);
        $userAttached = false;

        if ($cart->getUser() !== $user) {
            $this->setUser($cart, $user);
            $userAttached = $cart->getUser() === $user;
        }

        $this->requestStack->getSession()?->set('cart', $cart->getId()->toString());

        return $duplicatesRemoved || $userAttached;
    }

    private function cleanupDuplicateUserCarts(User $user, ?Cart $cartToKeep = null): bool
    {
        $removed = false;

        foreach ($this->cartRepository->findCartsInProgressByUser($user) as $existingCart) {
            if (
                $cartToKeep instanceof Cart
                && $existingCart->getId()->toString() === $cartToKeep->getId()->toString()
            ) {
                continue;
            }

            $this->cartRepository->delete($existingCart);
            $removed = true;
        }

        return $removed;
    }

    private function setUser(Cart $cart, ?User $user = null): void
    {
        $user ??= $this->security->getUser();

        if (!$user instanceof User) {
            return;
        }

        if (null !== $cart->getUser() && $cart->getUser() !== $user) {
            return;
        }

        $cart->setUser($user);

        if (null === $cart->getInvoiceAddress() && null !== $user->getInvoiceAddress()) {
            $invoiceAddress = new InvoiceAddress();
            $invoiceAddress->copy($user->getInvoiceAddress());
            $cart->setInvoiceAddress($invoiceAddress);
        }

        if (null !== $cart->getDeliveryAddress()) {
            return;
        }

        if (null !== $user->getDeliveryAddress()) {
            $deliveryAddress = new DeliveryAddress();
            $deliveryAddress->copy($user->getDeliveryAddress());
            $cart->setDeliveryAddress($deliveryAddress);

            return;
        }

        if (null !== $user->getInvoiceAddress()) {
            $deliveryAddress = new DeliveryAddress();
            $deliveryAddress->copy($user->getInvoiceAddress());
            $cart->setDeliveryAddress($deliveryAddress);
        }
    }

    private function resetUnavailableDeliveryMethod(Cart $cart): void
    {
        $deliveryMethod = $cart->getDeliveryMethod();

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

        try {
            $context = $this->shippingContextFactory->createFromCart($cart)->withDeliveryMethod($deliveryMethod);
        } catch (\Throwable) {
            return;
        }

        $country = $this->resolveCountry($context->countryCode, $context->priceGroupId);
        $county = $this->resolveCounty($context->postalCode, $country);
        $typeDefault = $this->productTypeRepository->findDefaultType();
        $types = $this->resolveCartTypes($cart);

        try {
            $this->shippingValidator->validate($context, $country, $county, $typeDefault, $types);
        } catch (\Throwable) {
            $cart->setDeliveryMethod(null);
            $cart->setCartShipping(null);
        }
    }

    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);
    }
}
