<?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\Bridge\Symfony\Bundle\Context\GroupContext;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Form\Site\Order\CheckoutDeliveryMethodViewBuilder;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Form\Site\Order\CheckoutType;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Manager\CheckoutPaymentSubmissionBuilder;
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\CartPromo;
use CpCreation\VitiCore\Cart\CartService;
use CpCreation\VitiCore\Cart\Model\Cart;
use CpCreation\VitiCore\Cart\Model\DeliveryAddress;
use CpCreation\VitiCore\Cart\Model\InvoiceAddress;
use CpCreation\VitiCore\Cart\Model\PaymentMethod;
use CpCreation\VitiCore\Cart\Repository\CartRepository;
use CpCreation\VitiCore\Contact\Repository\ContactRepository;
use CpCreation\VitiCore\Product\Model\ProductPriceCode;
use CpCreation\VitiCore\Product\Repository\ProductPriceCodeRepository;
use CpCreation\VitiCore\Product\Repository\ProductPriceGroupRepository;
use CpCreation\VitiCore\Shop\Repository\ShopConfigRepository;
use CpCreation\VitiCore\User\Model\User;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\ComponentToolsTrait;
use Symfony\UX\LiveComponent\ComponentWithFormTrait;
use Symfony\UX\LiveComponent\DefaultActionTrait;

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

    #[LiveProp(writable: true, fieldName: 'cartModel')]
    public ?Cart $cart = null;

    #[LiveProp(writable: false)]
    public ?string $cartId = null;

    #[LiveProp(writable: true)]
    public ?string $promoCodeInput = null;

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

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

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

    /**
     * @var array<string, string>
     */
    public array $promoCodeMessageParameters = [];

    private const string PROMO_TRANSIENT_MESSAGE_KEY = 'checkoutPromoTransientMessageKey';

    public function __construct(
        private readonly CartService $cartService,
        private readonly CartRepository $cartRepository,
        private readonly CheckoutDeliveryMethodViewBuilder $checkoutDeliveryMethodViewBuilder,
        private readonly ContactRepository $contactRepository,
        private readonly CheckoutPaymentSubmissionBuilder $checkoutPaymentSubmissionBuilder,
        private readonly ValidatorInterface $validator,
        private readonly Security $security,
        private readonly MessageBusInterface $commandSimpleBus,
        private readonly TranslatorInterface $translator,
        private readonly ProductPriceGroupRepository $productPriceGroupRepository,
        private readonly ProductPriceCodeRepository $productPriceCodeRepository,
        private readonly ShopConfigRepository $shopConfigRepository,
        private readonly RequestStack $requestStack,
        private readonly MatomoEcommerceTracker $matomoEcommerceTracker,
        private readonly MetaConversionsApiTracker $metaConversionsApiTracker,
    ) {}

    public ?string $deleteItemId = null;

    public function getActivePriceGroupName(): ?string
    {
        $groupId = GroupContext::$groupId;
        $group = \is_string($groupId) && '' !== $groupId
            ? $this->productPriceGroupRepository->find($groupId)
            : null;

        $group ??= $this->productPriceGroupRepository->getGroupByDefault();

        if (null === $group || ($group->isByDefault() && null === GroupContext::$partnerCode)) {
            return null;
        }

        return $group->getName();
    }

    protected function instantiateForm(): FormInterface
    {
        $this->cart = null !== $this->cartId
            ? $this->cartRepository->findOneCartInProgressById($this->cartId, false)
            : $this->cartService->getCart();

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

        if ($this->cart instanceof Cart && $this->synchronizeAuthenticatedCheckoutAddresses($this->cart)) {
            $this->cart = $this->cartService->hydrateCart($this->cart);
        }

        if ($this->cart instanceof Cart) {
            $this->consumeTransientPromoMessage($this->cart);
        }

        return $this->createForm(CheckoutType::class, $this->cart);
    }

    #[LiveAction]
    public function refreshShipping(): void
    {
        try {
            $this->submitForm(false);
        } catch (UnprocessableEntityHttpException) {
            // On garde quand même les champs déjà mappés
        }

        $cart = $this->getForm()->getData();

        if (!$cart instanceof Cart) {
            return;
        }

        $this->cartService->hydrateShipping($cart);
        $this->cartService->hydratePromos($cart);
        $this->cartService->hydratePricing($cart);

        $this->cart = $cart;
        $this->cartRepository->save($cart);
    }

    #[LiveAction]
    public function saveDraft(): void
    {
        try {
            $this->submitForm(false);
        } catch (UnprocessableEntityHttpException) {
            // On persiste quand même les champs déjà mappés.
        }

        $cart = $this->getForm()->getData();

        if (!$cart instanceof Cart) {
            return;
        }

        $synchronized = $this->synchronizeAuthenticatedCheckoutAddresses($cart);
        $this->cart = $this->cartService->hydrateCart($cart);

        if ($synchronized) {
            $this->resetForm();
        }
    }

    #[LiveAction]
    public function saveEmail(): void
    {
        $formName = $this->getFormName();
        $this->validatedFields = [
            $formName,
            $formName . '.deliveryAddress',
            $formName . '.deliveryAddress.email',
        ];

        try {
            $this->submitForm(false);
        } catch (UnprocessableEntityHttpException) {
            // On conserve l'erreur du champ email uniquement.
        }

        $cart = $this->getForm()->getData();

        if (!$cart instanceof Cart) {
            return;
        }

        $synchronized = $this->synchronizeAuthenticatedCheckoutAddresses($cart);
        $this->cart = $this->cartService->hydrateCart($cart);

        if ($synchronized) {
            $this->resetForm();
        }
    }

    #[LiveAction]
    public function applyPromoCode(): ?RedirectResponse
    {
        try {
            $this->submitForm(false);
        } catch (UnprocessableEntityHttpException) {
            // On garde les valeurs déjà mappées sans afficher les erreurs du checkout
        }

        $cart = $this->getForm()->getData();

        if (!$cart instanceof Cart) {
            return null;
        }

        $synchronized = $this->synchronizeAuthenticatedCheckoutAddresses($cart);
        $submittedCode = null !== $this->promoCodeInput ? mb_strtoupper(mb_trim($this->promoCodeInput)) : null;

        if (null !== $submittedCode && '' !== $submittedCode) {
            $priceCode = $this->productPriceCodeRepository->findOneCode($submittedCode);

            if ($priceCode instanceof ProductPriceCode && null !== $priceCode->getGroup()) {
                $alreadyActive = $this->isPartnerCodeAlreadyActive($priceCode);
                $this->applyPartnerCode($cart, $priceCode);
                $this->setTransientPromoMessage(
                    $alreadyActive
                        ? 'cart.promo.partner_code_already_active'
                        : 'cart.promo.partner_code_applied'
                );
                $this->cart = $cart;
                $this->promoCodeInput = null;
                $this->cartRepository->save($cart);

                return $this->redirectToCurrentPage();
            }
        }

        $previousPromoCodeInput = $cart->getPromoCodeInput();
        $cart->setPromoCodeInput($this->promoCodeInput);
        $this->cartService->hydrateShipping($cart);
        $this->cartService->hydratePromos($cart);
        $this->cartService->hydratePricing($cart);

        $this->cart = $cart;
        $saveCart = true;

        if (CartPromo::STATUS_INVALID === $cart->getPromoCodeStatus()) {
            $invalidMessageKey = $cart->getPromoCodeMessageKey();
            $this->promoCodeMessageParameters = $this->buildPromoCodeMessageParameters($submittedCode);
            $cart->setPromoCodeInput($previousPromoCodeInput);
            $this->cartService->hydratePromos($cart);
            $this->cartService->hydratePricing($cart);
            $this->cartRepository->save($cart);
            $cart->setPromoCodeStatus(CartPromo::STATUS_INVALID);
            $cart->setPromoCodeMessageKey($invalidMessageKey);
            $saveCart = false;
        }

        $this->promoCodeInput = CartPromo::STATUS_APPLIED === $cart->getPromoCodeStatus()
            ? null
            : $this->promoCodeInput;

        if ($saveCart) {
            $this->cartRepository->save($cart);
        }

        if ($synchronized) {
            $this->resetForm();
        }

        return null;
    }

    private function applyPartnerCode(Cart $cart, ProductPriceCode $priceCode): void
    {
        $group = $priceCode->getGroup();

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

        $session = $this->requestStack->getSession();
        $session->set('groupPrice', $group->getId()->toString());
        $session->set('codePrice', $priceCode->getId()->toString());
        $session->set('partnerCode', $priceCode->getCode());
        $session->set('groupByDefault', false);

        GroupContext::$groupId = $group->getId()->toString();
        GroupContext::$partnerCode = $priceCode->getCode();

        $cart->setGroupCode($priceCode->getCode());
        $cart->clearShipping();
        $this->cartService->hydratePromos($cart);
        $cart->clearPricing();
    }

    private function isPartnerCodeAlreadyActive(ProductPriceCode $priceCode): bool
    {
        $session = $this->requestStack->getSession();
        $currentCodePriceId = $session->get('codePrice');

        if (\is_string($currentCodePriceId) && $currentCodePriceId === $priceCode->getId()->toString()) {
            return true;
        }

        $currentPartnerCode = $session->get('partnerCode') ?? GroupContext::$partnerCode;

        if (!\is_string($currentPartnerCode)) {
            return false;
        }

        $normalizedCurrentPartnerCode = ProductPriceCodeRepository::normalizeCode($currentPartnerCode);
        $normalizedPriceCode = ProductPriceCodeRepository::normalizeCode((string) $priceCode->getCode());

        return $normalizedCurrentPartnerCode === $normalizedPriceCode;
    }

    /**
     * @return array<string, string>
     */
    private function buildPromoCodeMessageParameters(?string $code): array
    {
        if (null === $code || '' === mb_trim($code)) {
            return [];
        }

        return ['%code%' => mb_strtoupper(mb_trim($code))];
    }

    private function setTransientPromoMessage(string $messageKey): void
    {
        $this->requestStack->getSession()->set(self::PROMO_TRANSIENT_MESSAGE_KEY, $messageKey);
    }

    private function consumeTransientPromoMessage(Cart $cart): void
    {
        $session = $this->requestStack->getSession();
        $messageKey = $session->get(self::PROMO_TRANSIENT_MESSAGE_KEY);
        $session->remove(self::PROMO_TRANSIENT_MESSAGE_KEY);

        if (!\is_string($messageKey) || '' === $messageKey) {
            return;
        }

        $cart->setPromoCodeStatus(CartPromo::STATUS_PENDING_ADDRESS);
        $cart->setPromoCodeMessageKey($messageKey);
        $this->promoCodeMessageParameters = [];
    }

    private function redirectToCurrentPage(): RedirectResponse
    {
        $request = $this->requestStack->getCurrentRequest();
        $url = $request?->headers->get('X-Live-Url')
            ?? $request?->headers->get('referer')
            ?? '/';

        return $this->redirect($url);
    }

    #[LiveAction]
    public function savePaymentMethod(): void
    {
        try {
            $this->submitForm(false);
        } catch (UnprocessableEntityHttpException) {
        }

        $cart = $this->getForm()->getData();

        if (!$cart instanceof Cart) {
            return;
        }

        $synchronized = $this->synchronizeAuthenticatedCheckoutAddresses($cart);
        $this->cart = $cart;
        $this->cartRepository->save($cart);

        if ($synchronized) {
            $this->resetForm();
        }
    }

    #[LiveAction]
    public function resetDeliveryAddress(): void
    {
        try {
            $this->submitForm(false);
        } catch (UnprocessableEntityHttpException) {
            // On repart de l'état saisi courant avant de le remplacer.
        }

        $cart = $this->getForm()->getData();

        if (!$cart instanceof Cart) {
            return;
        }

        $this->suppressDeliveryAutofill = true;
        $this->clearDeliveryAddress($cart);
        $this->cart = $this->cartService->hydrateCart($cart);
        $this->resetForm();
    }

    #[LiveAction]
    public function resetInvoiceAddress(): void
    {
        try {
            $this->submitForm(false);
        } catch (UnprocessableEntityHttpException) {
            // On repart de l'état saisi courant avant de le remplacer.
        }

        $cart = $this->getForm()->getData();

        if (!$cart instanceof Cart) {
            return;
        }

        $invoiceAddress = $cart->getInvoiceAddress();

        if (!$invoiceAddress instanceof InvoiceAddress || !$invoiceAddress->isDifferentAddress()) {
            return;
        }

        $this->suppressInvoiceAutofill = true;
        $this->clearInvoiceAddress($cart);
        $this->cart = $this->cartService->hydrateCart($cart);
        $this->resetForm();
    }

    #[LiveAction]
    public function saveCgv(): void
    {
        $this->saveDraft();
    }

    public function isCartPayable(): bool
    {
        $cart = $this->getForm()->getData();

        return $cart instanceof Cart && !$cart->isCheckoutPayed() && null === $this->getCartBlockingIssue($cart);
    }

    public function getCartBlockingReason(): ?string
    {
        $cart = $this->getForm()->getData();

        if (!$cart instanceof Cart) {
            return $this->translator->trans('checkout_design.payment_blocked');
        }

        if ($cart->isCheckoutPayed()) {
            return $this->translator->trans('checkout_design.already_paid');
        }

        $issue = $this->getCartBlockingIssue($cart);

        return $issue['reason'] ?? null;
    }

    public function getPaidPaymentMethodLabel(): ?string
    {
        $cart = $this->getForm()->getData();

        if (!$cart instanceof Cart || !$cart->isCheckoutPayed()) {
            return null;
        }

        return $this->resolvePaymentMethodLabel($cart->getPaymentMethod());
    }

    /**
     * @return array{reason: string, selector: ?string}|null
     */
    private function getCartBlockingIssue(Cart $cart): ?array
    {
        $orderingBlockedReason = $this->getOrderingBlockedReason();

        if (null !== $orderingBlockedReason) {
            return [
                'reason' => $orderingBlockedReason,
                'selector' => null,
            ];
        }

        if ($cart->getItems()->isEmpty()) {
            return [
                'reason' => $this->translator->trans('checkout_design.payment_blocked'),
                'selector' => null,
            ];
        }

        $addressIssue = $this->getAddressBlockingIssue($cart);

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

        if (null === $cart->getPaymentMethod()) {
            return [
                'reason' => $this->translator->trans('checkout_design.payment_method_required'),
                'selector' => $this->buildSelectorForFormPath(['paymentMethod']),
            ];
        }

        $violations = $this->validator->validate($cart, null, $this->getValidationGroups($cart));

        if (\count($violations) > 0) {
            $violation = $violations[0];

            return [
                'reason' => $violation->getMessage(),
                'selector' => $this->buildSelectorFromPropertyPath((string) $violation->getPropertyPath()),
            ];
        }

        if (null === $cart->getDeliveryMethod()) {
            $standardChoice = $this->checkoutDeliveryMethodViewBuilder->buildStandardChoice($cart, true);

            if (null === $standardChoice || !$standardChoice['available']) {
                return [
                    'reason' => $standardChoice['unavailable_reason'] ?? $this->translator->trans('checkout_design.delivery_blocked'),
                    'selector' => $this->buildSelectorForFormPath(['deliveryMethod']),
                ];
            }

            return null;
        }

        $groups = $this->checkoutDeliveryMethodViewBuilder->build($cart, [$cart->getDeliveryMethod()], $cart->getDeliveryMethod());

        foreach ($groups as $group) {
            foreach ($group['choices'] as $choice) {
                if ($choice['method']->getId()->toString() !== $cart->getDeliveryMethod()?->getId()->toString()) {
                    continue;
                }

                if (!$choice['available']) {
                    return [
                        'reason' => $choice['unavailable_reason'] ?? $this->translator->trans('checkout_design.delivery_blocked'),
                        'selector' => $this->buildSelectorForFormPath(['deliveryMethod']),
                    ];
                }

                return null;
            }
        }

        return [
            'reason' => $this->translator->trans('checkout_design.delivery_blocked'),
            'selector' => $this->buildSelectorForFormPath(['deliveryMethod']),
        ];
    }

    private function getOrderingBlockedReason(): ?string
    {
        $config = $this->shopConfigRepository->findOne();

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

        if ($config->isOrderingDisabledForVacation()) {
            return $this->translator->trans('shop.orders_disabled_for_vacation');
        }

        $partnerCode = $this->requestStack->getSession()?->get('partnerCode');
        $partnerCode = \is_string($partnerCode) ? $partnerCode : null;

        return $config->shouldShowPrices($partnerCode)
            ? null
            : $this->translator->trans('shop.view_with_partner_code');
    }

    /**
     * @return array{reason: string, selector: ?string}|null
     */
    private function getAddressBlockingIssue(Cart $cart): ?array
    {
        $deliverySelector = $this->findFirstMissingAddressSelector(
            'deliveryAddress',
            $cart->getDeliveryAddress(),
            true,
        );

        if (null !== $deliverySelector) {
            return [
                'reason' => $this->translator->trans('checkout_design.billing_address_required'),
                'selector' => $deliverySelector,
            ];
        }

        $invoiceAddress = $cart->getInvoiceAddress();

        if ($invoiceAddress instanceof InvoiceAddress && $invoiceAddress->isDifferentAddress()) {
            $invoiceSelector = $this->findFirstMissingAddressSelector(
                'invoiceAddress',
                $invoiceAddress,
                true,
            );

            if (null !== $invoiceSelector) {
                return [
                    'reason' => $this->translator->trans('checkout_design.invoice_address_required'),
                    'selector' => $invoiceSelector,
                ];
            }
        }

        return null;
    }

    #[LiveAction]
    public function payNow(): ?RedirectResponse
    {
        try {
            $this->submitForm();
        } catch (UnprocessableEntityHttpException) {
            $message = $this->form->getErrors(true)->count() > 0
                ? $this->form->getErrors(true)->current()->getMessage()
                : $this->translator->trans('checkout_design.form_invalid');

            $this->commandSimpleBus->dispatch(new NotificationCommand(
                message: $message,
                level: PushLevel::ERROR,
            ));
            $selector = $this->findFirstInvalidFormSelector();

            if (null !== $selector) {
                $this->dispatchBrowserEvent('checkout:focus-field', ['selector' => $selector]);
            }

            return null;
        }

        $cart = $this->getForm()->getData();

        if (!$cart instanceof Cart) {
            return null;
        }

        if ($cart->isCheckoutPayed()) {
            $this->commandSimpleBus->dispatch(new NotificationCommand(
                message: $this->translator->trans('checkout_design.already_paid'),
                level: PushLevel::ERROR,
            ));

            return null;
        }

        $this->synchronizeAuthenticatedCheckoutAddresses($cart);
        $this->cart = $this->cartService->hydrateCart($cart);
        $issue = $this->getCartBlockingIssue($this->cart);

        if (null !== $issue) {
            $this->commandSimpleBus->dispatch(new NotificationCommand(
                message: $issue['reason'],
                level: PushLevel::ERROR,
            ));
            if (null !== $issue['selector']) {
                $this->dispatchBrowserEvent('checkout:focus-field', ['selector' => $issue['selector']]);
            }

            return null;
        }

        if (!$this->validateStockLimits($this->cart)) {
            return null;
        }

        $this->cartRepository->save($this->cart);
        $contact = $this->contactRepository->findOne();

        if (null === $contact) {
            $this->commandSimpleBus->dispatch(new NotificationCommand(
                message: $this->translator->trans('checkout_design.payment_blocked'),
                level: PushLevel::ERROR,
            ));

            return null;
        }

        $submission = $this->checkoutPaymentSubmissionBuilder->build($contact, $this->cart);

        $checkout = $this->cart->getCartCheckout();
        $isOnlinePayment = $this->cart->getPaymentMethod()?->isCheckoutOnlineMethod() ?? true;
        $checkoutUpdated = false;

        if (true === $isOnlinePayment) {
            $checkout->setInitiatedAt(new \DateTimeImmutable());
            $checkoutUpdated = true;
        }

        if (null === $checkout?->getOrderedAt() && false === $isOnlinePayment) {
            $checkout->setOrderedAt(new \DateTimeImmutable());
            $checkoutUpdated = true;
        }

        if ($checkoutUpdated) {
            $this->cartRepository->save($this->cart);
        }

        $request = $this->requestStack->getCurrentRequest();
        $this->matomoEcommerceTracker->trackInitiateCheckout($this->cart, $request);
        $this->metaConversionsApiTracker->trackAddPaymentInfo($this->cart, $request);

        $this->dispatchBrowserEvent('checkout:submit-payment', $submission);

        return null;
    }

    private function findFirstInvalidFormSelector(): ?string
    {
        foreach ($this->form->getErrors(true) as $error) {
            $origin = $error->getOrigin();

            if (!$origin instanceof FormInterface) {
                continue;
            }

            $selector = $this->buildSelectorFromForm($origin);

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

        return null;
    }

    private function validateStockLimits(Cart $cart): bool
    {
        foreach ($cart->getItems() as $item) {
            $stockViolation = $this->cartService->getStockViolationForCartItem(
                $item,
                $cart,
                $item,
                true,
            );

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

            $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 false;
        }

        return true;
    }

    private function buildSelectorFromPropertyPath(string $propertyPath): ?string
    {
        if ('' === $propertyPath) {
            return null;
        }

        preg_match_all('/[A-Za-z0-9_]+/', $propertyPath, $matches);
        $segments = $matches[0] ?? [];

        if ([] === $segments) {
            return null;
        }

        return $this->buildSelectorForFormPath($segments);
    }

    /**
     * @param list<string> $pathSegments
     */
    private function buildSelectorForFormPath(array $pathSegments): ?string
    {
        $field = $this->getForm();

        foreach ($pathSegments as $segment) {
            if (!$field->has($segment)) {
                return null;
            }

            $field = $field->get($segment);
        }

        return $this->buildSelectorFromForm($field);
    }

    private function buildSelectorFromForm(FormInterface $field): ?string
    {
        $parts = [$field->getName()];
        $parent = $field->getParent();

        while (null !== $parent) {
            $name = $parent->getName();

            if ('' !== $name) {
                array_unshift($parts, $name);
            }

            $parent = $parent->getParent();
        }

        if ([] === $parts) {
            return null;
        }

        $inputName = array_shift($parts);

        if (null === $inputName || '' === $inputName) {
            return null;
        }

        foreach ($parts as $part) {
            $inputName .= '[' . $part . ']';
        }

        return \sprintf('[name="%s"]', $inputName);
    }

    private function findFirstMissingAddressSelector(
        string $rootField,
        DeliveryAddress|InvoiceAddress|null $address,
        bool $includeEmail,
    ): ?string {
        if (null === $address) {
            return $this->buildSelectorForFormPath([$rootField, 'firstname']);
        }

        $fields = [
            'firstname' => $address->getFirstname(),
            'lastname' => $address->getLastname(),
            'street' => $address->getStreet(),
            'postal' => $address->getPostal(),
            'city' => $address->getCity(),
            'country' => $address->getCountry(),
            'phone' => $address->getPhone(),
        ];

        if ($includeEmail && $address instanceof DeliveryAddress) {
            $fields['email'] = $address->getEmail();
        }

        foreach ($fields as $field => $value) {
            if (null === $value || '' === mb_trim($value)) {
                return $this->buildSelectorForFormPath([$rootField, $field]);
            }
        }

        return null;
    }

    /**
     * @return list<string>
     */
    private function getValidationGroups(Cart $cart): array
    {
        $groups = ['Default', 'delivery_address'];

        if ($cart->getInvoiceAddress()?->isDifferentAddress()) {
            $groups[] = 'invoice_address';
        }

        return $groups;
    }

    private function resolvePaymentMethodLabel(?PaymentMethod $paymentMethod): ?string
    {
        if (null === $paymentMethod) {
            return null;
        }

        $configuration = method_exists($paymentMethod, 'getConfiguration')
            ? $paymentMethod->getConfiguration()
            : [];

        $configuredName = $configuration['name'] ?? null;
        if (\is_string($configuredName) && '' !== $configuredName) {
            return $configuredName;
        }

        $method = $paymentMethod->getMethod();

        return match ($method) {
            'check' => $this->translator->trans('checkout_design.payment_check'),
            'bankTransfert', 'banktransfert' => $this->translator->trans('checkout_design.payment_banktransfert'),
            default => $paymentMethod->isCheckoutOnlineMethod()
                ? $this->translator->trans('checkout_design.payment_card')
                : $method,
        };
    }

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

        $changed = $this->clearInvoiceAddressWhenDisabled($cart);

        if (!$cart->getInvoiceAddress()?->isDifferentAddress()) {
            $this->suppressInvoiceAutofill = false;
        }

        if ($this->disableUserAutofill) {
            return $changed;
        }

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

        if (!$this->suppressDeliveryAutofill && $this->shouldPrefillDeliveryAddress($cart)) {
            $changed = $this->copyDeliveryAddressFromUser($cart, $user, true) || $changed;
        } else {
            $changed = $this->fillDeliveryEmailFromUser($cart, $user) || $changed;
        }

        if (
            !$this->suppressInvoiceAutofill
            && $cart->getInvoiceAddress()?->isDifferentAddress()
            && $this->shouldPrefillInvoiceAddress($cart)
        ) {
            $changed = $this->copyInvoiceAddressFromUser($cart, $user, true) || $changed;
        }

        return $changed;
    }

    private function shouldPrefillDeliveryAddress(Cart $cart): bool
    {
        return $this->isDeliveryAddressCoreEmpty($cart->getDeliveryAddress());
    }

    private function shouldPrefillInvoiceAddress(Cart $cart): bool
    {
        return $this->isInvoiceAddressCoreEmpty($cart->getInvoiceAddress());
    }

    private function copyDeliveryAddressFromUser(Cart $cart, User $user, bool $force): bool
    {
        $source = $user->getDeliveryAddress() ?? $user->getInvoiceAddress();

        if (null === $source) {
            return $this->fillDeliveryEmailFromUser($cart, $user);
        }

        $target = $cart->getDeliveryAddress();
        $before = $this->snapshotDeliveryAddress($target);

        if (!$target instanceof DeliveryAddress) {
            $target = new DeliveryAddress();
            $cart->setDeliveryAddress($target);
        }

        if ($force || $this->isDeliveryAddressCoreEmpty($target)) {
            $target->copy($source);
        }

        $userEmail = mb_trim((string) $user->getEmail());

        if ($force && '' !== $userEmail) {
            $target->setEmail($userEmail);
        } elseif (null === $target->getEmail() || '' === mb_trim((string) $target->getEmail())) {
            $target->setEmail($user->getEmail());
        }

        return $before !== $this->snapshotDeliveryAddress($target);
    }

    private function copyInvoiceAddressFromUser(Cart $cart, User $user, bool $force): bool
    {
        $source = $user->getInvoiceAddress() ?? $user->getDeliveryAddress();

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

        $target = $cart->getInvoiceAddress();
        $before = $this->snapshotInvoiceAddress($target);

        if (!$target instanceof InvoiceAddress) {
            $target = new InvoiceAddress();
            $target->setDifferentAddress(true);
            $cart->setInvoiceAddress($target);
        }

        if ($force || $this->isInvoiceAddressCoreEmpty($target)) {
            $target->copy($source);
        }

        return $before !== $this->snapshotInvoiceAddress($target);
    }

    private function fillDeliveryEmailFromUser(Cart $cart, User $user): bool
    {
        $deliveryAddress = $cart->getDeliveryAddress();
        $userEmail = mb_trim((string) $user->getEmail());

        if (!$deliveryAddress instanceof DeliveryAddress) {
            if ('' === $userEmail) {
                return false;
            }

            $deliveryAddress = new DeliveryAddress();
            $deliveryAddress->setEmail($userEmail);
            $cart->setDeliveryAddress($deliveryAddress);

            return true;
        }

        $currentEmail = mb_trim((string) $deliveryAddress->getEmail());

        if ('' !== $currentEmail || '' === $userEmail) {
            return false;
        }

        $deliveryAddress->setEmail($userEmail);

        return true;
    }

    private function clearInvoiceAddressWhenDisabled(Cart $cart): bool
    {
        $invoiceAddress = $cart->getInvoiceAddress();

        if (!$invoiceAddress instanceof InvoiceAddress || $invoiceAddress->isDifferentAddress()) {
            return false;
        }

        $before = $this->snapshotInvoiceAddress($invoiceAddress);

        $invoiceAddress->setCompany(null);
        $invoiceAddress->setSiret(null);
        $invoiceAddress->setVatId(null);
        $invoiceAddress->setLastname(null);
        $invoiceAddress->setFirstname(null);
        $invoiceAddress->setStreet(null);
        $invoiceAddress->setPostal(null);
        $invoiceAddress->setCity(null);
        $invoiceAddress->setCountry(null);
        $invoiceAddress->setPhone(null);
        $invoiceAddress->setEmail(null);

        return $before !== $this->snapshotInvoiceAddress($invoiceAddress);
    }

    private function clearDeliveryAddress(Cart $cart): void
    {
        $deliveryAddress = $cart->getDeliveryAddress();

        if (!$deliveryAddress instanceof DeliveryAddress) {
            return;
        }

        $deliveryAddress->setCompany(null);
        $deliveryAddress->setSiret(null);
        $deliveryAddress->setVatId(null);
        $deliveryAddress->setLastname(null);
        $deliveryAddress->setFirstname(null);
        $deliveryAddress->setStreet(null);
        $deliveryAddress->setPostal(null);
        $deliveryAddress->setCity(null);
        $deliveryAddress->setCountry(null);
        $deliveryAddress->setPhone(null);
        $deliveryAddress->setEmail(null);
    }

    private function clearInvoiceAddress(Cart $cart): void
    {
        $invoiceAddress = $cart->getInvoiceAddress();

        if (!$invoiceAddress instanceof InvoiceAddress) {
            return;
        }

        $invoiceAddress->setCompany(null);
        $invoiceAddress->setSiret(null);
        $invoiceAddress->setVatId(null);
        $invoiceAddress->setLastname(null);
        $invoiceAddress->setFirstname(null);
        $invoiceAddress->setStreet(null);
        $invoiceAddress->setPostal(null);
        $invoiceAddress->setCity(null);
        $invoiceAddress->setCountry(null);
        $invoiceAddress->setPhone(null);
        $invoiceAddress->setEmail(null);
    }

    private function isDeliveryAddressCoreEmpty(?DeliveryAddress $address): bool
    {
        if (!$address instanceof DeliveryAddress) {
            return true;
        }

        return $this->isAddressCoreEmpty(
            $address->getFirstname(),
            $address->getLastname(),
            $address->getStreet(),
            $address->getPostal(),
            $address->getCity(),
            $address->getPhone(),
        );
    }

    private function isInvoiceAddressCoreEmpty(?InvoiceAddress $address): bool
    {
        if (!$address instanceof InvoiceAddress) {
            return true;
        }

        return $this->isAddressCoreEmpty(
            $address->getFirstname(),
            $address->getLastname(),
            $address->getStreet(),
            $address->getPostal(),
            $address->getCity(),
            $address->getPhone(),
        );
    }

    private function isAddressCoreEmpty(?string ...$values): bool
    {
        foreach ($values as $value) {
            if (null !== $value && '' !== mb_trim($value)) {
                return false;
            }
        }

        return true;
    }

    /**
     * @return array<string, mixed>
     */
    private function snapshotDeliveryAddress(?DeliveryAddress $address): array
    {
        if (!$address instanceof DeliveryAddress) {
            return [];
        }

        return [
            'firstname' => $address->getFirstname(),
            'lastname' => $address->getLastname(),
            'company' => $address->getCompany(),
            'siret' => $address->getSiret(),
            'vatId' => $address->getVatId(),
            'street' => $address->getStreet(),
            'postal' => $address->getPostal(),
            'city' => $address->getCity(),
            'country' => $address->getCountry(),
            'phone' => $address->getPhone(),
            'email' => $address->getEmail(),
        ];
    }

    /**
     * @return array<string, mixed>
     */
    private function snapshotInvoiceAddress(?InvoiceAddress $address): array
    {
        if (!$address instanceof InvoiceAddress) {
            return [];
        }

        return [
            'firstname' => $address->getFirstname(),
            'lastname' => $address->getLastname(),
            'company' => $address->getCompany(),
            'siret' => $address->getSiret(),
            'vatId' => $address->getVatId(),
            'street' => $address->getStreet(),
            'postal' => $address->getPostal(),
            'city' => $address->getCity(),
            'country' => $address->getCountry(),
            'phone' => $address->getPhone(),
            'email' => $address->getEmail(),
            'differentAddress' => $address->isDifferentAddress(),
        ];
    }
}
