<?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\Bridge\Symfony\Bundle\Twig\Components\Cart;

use CpCreation\VitiCore\Award\Model\Award;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Context\GroupContext;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Form\Site\Order\CartItemType;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Notification\Command\NotificationCommand;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Notification\Enum\PushLevel;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Tracking\MatomoEcommerceTracker;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Tracking\MetaConversionsApiTracker;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Twig\Components\Behavior\HasTemplateOverride;
use CpCreation\VitiCore\Cart\CartService;
use CpCreation\VitiCore\Cart\Model\CartItem;
use CpCreation\VitiCore\Cart\Repository\CartRepository;
use CpCreation\VitiCore\Media\Model\Image as MediaImage;
use CpCreation\VitiCore\Product\Model\Product;
use CpCreation\VitiCore\Product\Model\ProductDegressive;
use CpCreation\VitiCore\Product\Model\ProductImage;
use CpCreation\VitiCore\Product\Model\ProductVariant;
use CpCreation\VitiCore\Product\Model\ProductVariantImage;
use CpCreation\VitiCore\Product\Model\ProductVariantOption;
use CpCreation\VitiCore\Product\Model\ProductVariantOptionImage;
use CpCreation\VitiCore\Product\Repository\ProductDegressiveRepository;
use CpCreation\VitiCore\Product\Repository\ProductPriceGroupRepository;
use CpCreation\VitiCore\Product\Repository\ProductVariantRepository;
use CpCreation\VitiCore\Shop\Model\ShopConfig;
use CpCreation\VitiCore\Shop\Repository\ShopConfigRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveArg;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\ComponentToolsTrait;
use Symfony\UX\LiveComponent\ComponentWithFormTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;
use Symfony\UX\TwigComponent\Attribute\PreMount;

#[AsLiveComponent('cart:ItemForm', template: '@components/cart/ItemForm.html.twig')]
class ItemForm extends AbstractController
{
    use ComponentToolsTrait;
    use ComponentWithFormTrait;
    use DefaultActionTrait;
    use HasTemplateOverride;

    private const LIMITED_STOCK_THRESHOLD = 20;

    public function __construct(
        private readonly MessageBusInterface $commandSimpleBus,
        private readonly TranslatorInterface $translator,
        private readonly CartService $cartService,
        private readonly CartRepository $cartRepository,
        private readonly ProductDegressiveRepository $productDegressiveRepository,
        private readonly ProductPriceGroupRepository $productPriceGroupRepository,
        private readonly ProductVariantRepository $productVariantRepository,
        private readonly ShopConfigRepository $shopConfigRepository,
        private readonly RequestStack $requestStack,
        private readonly MatomoEcommerceTracker $matomoEcommerceTracker,
        private readonly MetaConversionsApiTracker $metaConversionsApiTracker,
    ) {}

    #[LiveProp(writable: false)]
    public Product $product;

    #[LiveProp(writable: false)]
    public ?float $reviewRatingValue = null;

    #[LiveProp(writable: false)]
    public int $reviewCount = 0;

    #[LiveProp(writable: false)]
    public bool $reviewsEnabled = false;

    private ?ShopConfig $shopConfig = null;

    /**
     * @return list<MediaImage>
     */
    public function getGalleryImages(): array
    {
        $selectedOption = $this->getSelectedOption();
        if ($selectedOption instanceof ProductVariantOption) {
            $optionImages = $this->buildGalleryImagesFromOption($selectedOption);
            if ([] !== $optionImages) {
                return $optionImages;
            }
        }

        $selectedVariant = $this->getSelectedVariant();
        if ($selectedVariant instanceof ProductVariant) {
            $variantImages = $this->buildGalleryImagesFromVariant($selectedVariant);
            if ([] !== $variantImages) {
                return $variantImages;
            }
        }

        $images = $this->product->getImages()->toArray();
        usort($images, static function (ProductImage $left, ProductImage $right): int {
            return ($left->getPosition() ?? \PHP_INT_MAX) <=> ($right->getPosition() ?? \PHP_INT_MAX);
        });

        $galleryImages = [];
        $seenPaths = [];

        $primaryImage = $this->product->getSecondImage() ?? $this->product->getImage();
        if ($primaryImage instanceof MediaImage && $primaryImage->getPath()) {
            $galleryImages[] = $primaryImage;
            $seenPaths[$primaryImage->getPath()] = true;
        }

        foreach ($images as $image) {
            $media = $image->getImage();

            if (!$media instanceof MediaImage || !$media->getPath()) {
                continue;
            }

            if (isset($seenPaths[$media->getPath()])) {
                continue;
            }

            $galleryImages[] = $media;
            $seenPaths[$media->getPath()] = true;
        }

        return $galleryImages;
    }

    public function getSelectedOption(): ?ProductVariantOption
    {
        $variant = $this->getSelectedVariant();
        if (!$variant instanceof ProductVariant) {
            return null;
        }

        $selectedOptionId = $this->formValues['option'] ?? null;

        if (\is_string($selectedOptionId) && '' !== $selectedOptionId) {
            foreach ($variant->getOptions() as $option) {
                if ($option->getId()->toString() === $selectedOptionId && $option->isAvailable()) {
                    return $option;
                }
            }
        }

        return null;
    }

    public function getSelectedVariant(): ?ProductVariant
    {
        $selectedVariantId = $this->formValues['variant'] ?? null;

        if (\is_string($selectedVariantId) && '' !== $selectedVariantId) {
            foreach ($this->product->getVariants() as $variant) {
                if ($variant->getId()->toString() === $selectedVariantId) {
                    return $variant;
                }
            }
        }

        $variant = $this->product->getVariantByDefault();

        return $variant instanceof ProductVariant ? $variant : null;
    }

    public function isSelectedVariantHasDegressive(): bool
    {
        $variant = $this->getSelectedVariant();

        if (!$variant instanceof ProductVariant) {
            return false;
        }

        foreach ($this->productDegressiveRepository->findAllApplicableByProductAndByGroup($this->product, $this->resolvePriceGroupId()) as $rule) {
            if ($rule instanceof ProductDegressive && $rule->getTypes()->contains($variant->getType())) {
                return true;
            }
        }

        return false;
    }

    public function getSelectedVariantRemainingStock(): ?int
    {
        $variant = $this->getSelectedVariant();

        if (!$variant instanceof ProductVariant) {
            return null;
        }

        $remainingStock = $this->cartService->getRemainingStockForVariant(
            $variant,
            $this->cartService->getCart(),
            includeCurrentCartItems: true,
        );

        return null === $remainingStock ? null : max(0, $remainingStock);
    }

    public function isSelectedVariantLimitedStock(): bool
    {
        if (!$this->shouldEvaluateSelectedVariantLimitedStock()) {
            return false;
        }

        $variant = $this->getSelectedVariant();

        if (!$variant instanceof ProductVariant || !$variant->isAvailable()) {
            return false;
        }

        $remainingStock = $this->getSelectedVariantRemainingStock();

        return null !== $remainingStock
            && $remainingStock > 0
            && $remainingStock < self::LIMITED_STOCK_THRESHOLD;
    }

    public function isSelectedVariantOutOfStock(): bool
    {
        if (!$this->shouldEvaluateSelectedVariantStock()) {
            return false;
        }

        $variant = $this->getSelectedVariant();

        if (!$variant instanceof ProductVariant) {
            return false;
        }

        if (!$variant->isAvailable()) {
            return true;
        }

        $remainingStock = $this->getSelectedVariantRemainingStock();

        return null !== $remainingStock && $remainingStock <= 0;
    }

    public function hasSelectedVariantChoice(): bool
    {
        $selectedVariantId = $this->formValues['variant'] ?? null;

        return \is_string($selectedVariantId) && '' !== $selectedVariantId;
    }

    public function shouldShowSelectedVariantAvailabilityIndicators(): bool
    {
        return $this->shouldEvaluateSelectedVariantStock();
    }

    public function shouldShowSelectedVariantLimitedStockIndicator(): bool
    {
        return $this->shouldEvaluateSelectedVariantLimitedStock();
    }

    public function isProductOutOfStock(): bool
    {
        foreach ($this->product->getVariants() as $variant) {
            if (!$variant instanceof ProductVariant) {
                continue;
            }

            if (!$variant->isAvailable()) {
                continue;
            }

            $remainingStock = $this->cartService->getRemainingStockForVariant(
                $variant,
                $this->cartService->getCart(),
                includeCurrentCartItems: true,
            );

            if (null === $remainingStock || $remainingStock > 0) {
                return false;
            }
        }

        return true;
    }

    private function shouldEvaluateSelectedVariantStock(): bool
    {
        if ($this->hasSelectedVariantChoice()) {
            return true;
        }

        return $this->productVariantRepository->getVariantsByProduct($this->product)->count() <= 1;
    }

    private function shouldEvaluateSelectedVariantLimitedStock(): bool
    {
        if ($this->shouldEvaluateSelectedVariantStock()) {
            return true;
        }

        return $this->getSelectedVariant() instanceof ProductVariant;
    }

    public function isAwardShopEnabled(): bool
    {
        return $this->getShopConfig()?->isEnableAwardShop() ?? false;
    }

    public function isProductPageFrontEnabled(): bool
    {
        return $this->getShopConfig()?->isEnableProductPageFront() ?? false;
    }

    public function shouldShowPrices(): bool
    {
        $partnerCode = GroupContext::$partnerCode;

        if (!\is_string($partnerCode) || '' === mb_trim($partnerCode)) {
            $partnerCode = $this->requestStack->getSession()?->get('partnerCode');
        }

        $partnerCode = \is_string($partnerCode) ? $partnerCode : null;

        return $this->getShopConfig()?->shouldShowPrices($partnerCode) ?? true;
    }

    public function isOrderingDisabledForVacation(): bool
    {
        return $this->getShopConfig()?->isOrderingDisabledForVacation() ?? false;
    }

    public function getVacationAt(): ?\DateTime
    {
        return $this->getShopConfig()?->getVacationAt();
    }

    /**
     * @return list<Award>
     */
    public function getVisibleAwards(): array
    {
        if (!$this->isAwardShopEnabled()) {
            return [];
        }

        return array_values(array_filter(
            $this->product->getAwards()->toArray(),
            static fn(mixed $award): bool => $award instanceof Award
        ));
    }

    #[PreMount]
    public function preMount(array $data): array
    {
        $resolver = new OptionsResolver();
        $resolver->setRequired('product');
        $resolver->setDefaults([
            'reviewRatingValue' => null,
            'reviewCount' => 0,
            'reviewsEnabled' => false,
        ]);
        $resolver->setAllowedTypes('reviewRatingValue', ['null', 'float']);
        $resolver->setAllowedTypes('reviewCount', 'int');
        $resolver->setAllowedTypes('reviewsEnabled', 'bool');

        return $resolver->resolve($data);
    }

    protected function instantiateForm(): FormInterface
    {
        $item = CartItem::create($this->product);

        if ($this->productVariantRepository->getVariantsByProduct($this->product)->count() > 1) {
            $item->setVariant(null);
        } elseif ($item->getVariant() instanceof ProductVariant && ($item->getVariant()->getLot() ?? 1) > 1) {
            $item->setQuantity($item->getVariant()->getLot());
        }

        $item->setCart($this->cartService->getCart());

        // we can extend AbstractController to get the normal shortcuts
        return $this->createForm(CartItemType::class, $item, [
            'product' => $this->product,
            'option_expanded' => $this->isOptionExpanded(),
        ]);
    }

    protected function isOptionExpanded(): bool
    {
        return false;
    }

    #[LiveAction]
    public function save(): ?Response
    {
        if ($this->isOrderingDisabledForVacation() || !$this->shouldShowPrices()) {
            return null;
        }

        try {
            $this->submitForm();
            /** @var CartItem $item */
            $item = $this->form->getData();
            $item->applySelectedOption($this->getSelectedOption());

            $cart = $this->cartService->getCart();
            if (!$cart) {
                $cart = $this->cartService->createCart();
            }

            $stockViolation = $this->cartService->getStockViolationForCartItem(
                $item,
                $cart,
                includeCurrentCartItems: true,
            );

            if (null !== $stockViolation) {
                $this->commandSimpleBus->dispatch(new NotificationCommand(
                    message: $this->translator->trans('shop.error.stock_limit', [
                        '%name%' => $stockViolation['name'],
                        '%quantity%' => $stockViolation['requested_quantity'],
                        '%stock%' => $stockViolation['available_stock'],
                    ], 'validators'),
                    level: PushLevel::ERROR
                ));

                return null;
            }

            $cart->addItem($item);
            $this->cartRepository->save($cart);
            $notification = new NotificationCommand(
                message: $this->translator->trans('shop.success.added_to_cart', [
                    '%name%' => $this->buildAddedToCartItemName($item),
                    '%quantity%' => $item->getQuantity(),
                ]),
                level: PushLevel::SUCCESS
            );
            $this->commandSimpleBus->dispatch($notification);
            $this->trackAddToCartEvents($item);
            $this->emit('cartUpdated');
        } catch (UnprocessableEntityHttpException $e) {
            if ($this->form->getErrors(true)->count() > 0) {
                $notification = new NotificationCommand(
                    message: $this->form->getErrors(true)->current()->getMessage(),
                    level: PushLevel::ERROR
                );
                $this->commandSimpleBus->dispatch($notification);
            }
        }

        return null;
    }

    #[LiveAction]
    public function showDegressiveModal(#[LiveArg('variantId')] ?ProductVariant $variant = null): void
    {
        if (!$variant instanceof ProductVariant || $variant->getProduct()?->getId() !== $this->product->getId()) {
            $variant = $this->getSelectedVariant();
        }

        $payload = ['productId' => $this->product->getId()->toString()];

        if ($variant instanceof ProductVariant) {
            $payload['variantId'] = $variant->getId()->toString();
        }

        $this->emit('degressiveModal:open', $payload, 'modal:SimpleModalDegressivity');
    }

    #[LiveAction]
    public function showAwardModal(#[LiveArg('awardId')] ?Award $award = null): void
    {
        if (!$award instanceof Award || !$this->product->getAwards()->contains($award)) {
            return;
        }

        $this->emit('awardModal:open', [
            'productId' => $this->product->getId()->toString(),
            'awardId' => $award->getId()->toString(),
        ], 'modal:SimpleModalAward');
    }

    #[LiveAction]
    public function showProductDetailModal(): void
    {
        $this->emit('productDetailModal:open', [
            'productId' => $this->product->getId()->toString(),
        ], 'modal:SimpleModalProductDetail');
    }

    #[LiveAction]
    public function showProductReviewsModal(): void
    {
        if (!$this->reviewsEnabled) {
            return;
        }

        $this->emit('productReviewsModal:open', [
            'productId' => $this->product->getId()->toString(),
        ], 'modal:SimpleModalProductReviews');
    }

    private function resolvePriceGroupId(): ?string
    {
        $groupId = GroupContext::$groupId;

        if (!\is_string($groupId) || '' === $groupId) {
            $groupId = $this->requestStack->getSession()?->get('groupPrice');
        }

        if (!\is_string($groupId) || '' === $groupId) {
            $groupId = $this->productPriceGroupRepository->getGroupByDefault()?->getId()?->toString();
        }

        return \is_string($groupId) && '' !== $groupId ? $groupId : null;
    }

    private function getShopConfig(): ?ShopConfig
    {
        if (null !== $this->shopConfig) {
            return $this->shopConfig;
        }

        return $this->shopConfig = $this->shopConfigRepository->findOne();
    }

    private function trackAddToCartEvents(CartItem $item): void
    {
        $request = $this->requestStack->getCurrentRequest();
        $this->matomoEcommerceTracker->trackAddToCart($item, $request);
        $this->metaConversionsApiTracker->trackAddToCart($item, $request);
    }

    private function buildAddedToCartItemName(CartItem $item): string
    {
        $name = (string) ($item->getVariant()?->getFullName() ?: $this->product->getFullName());

        if (!$item->hasSelectedOption()) {
            return $name;
        }

        return \sprintf(
            '%s - %s : %s',
            $name,
            $this->translator->trans('shop.option_label'),
            $item->getSelectedOptionName()
        );
    }

    /**
     * @return list<MediaImage>
     */
    private function buildGalleryImagesFromVariant(ProductVariant $variant): array
    {
        $images = $variant->getImages()->toArray();
        usort($images, static function (ProductVariantImage $left, ProductVariantImage $right): int {
            return ($left->getPosition() ?? \PHP_INT_MAX) <=> ($right->getPosition() ?? \PHP_INT_MAX);
        });

        return $this->extractMediaImages($images, static fn(ProductVariantImage $image): ?MediaImage => $image->getImage());
    }

    /**
     * @return list<MediaImage>
     */
    private function buildGalleryImagesFromOption(ProductVariantOption $option): array
    {
        $images = $option->getImages()->toArray();
        usort($images, static function (ProductVariantOptionImage $left, ProductVariantOptionImage $right): int {
            return ($left->getPosition() ?? \PHP_INT_MAX) <=> ($right->getPosition() ?? \PHP_INT_MAX);
        });

        return $this->extractMediaImages($images, static fn(ProductVariantOptionImage $image): ?MediaImage => $image->getImage());
    }

    /**
     * @template T of object
     *
     * @param list<T>                  $items
     * @param callable(T): ?MediaImage $resolver
     *
     * @return list<MediaImage>
     */
    private function extractMediaImages(array $items, callable $resolver): array
    {
        $galleryImages = [];
        $seenPaths = [];

        foreach ($items as $item) {
            $media = $resolver($item);

            if (!$media instanceof MediaImage || !$media->getPath()) {
                continue;
            }

            if (isset($seenPaths[$media->getPath()])) {
                continue;
            }

            $galleryImages[] = $media;
            $seenPaths[$media->getPath()] = true;
        }

        return $galleryImages;
    }
}
