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

use CpCreation\VitiCore\Behavior\HasPayment;
use CpCreation\VitiCore\Behavior\HasTimestamp;
use CpCreation\VitiCore\Behavior\Impl\SoftDeletable;
use CpCreation\VitiCore\Behavior\Impl\Timestamp;
use CpCreation\VitiCore\Cart\CartPricing;
use CpCreation\VitiCore\Cart\CartPromo;
use CpCreation\VitiCore\Cart\CartPromos;
use CpCreation\VitiCore\Cart\CartShipping;
use CpCreation\VitiCore\Product\Model\Product;
use CpCreation\VitiCore\Shop\Model\DeliveryMethod;
use CpCreation\VitiCore\User\Model\User;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use Symfony\Component\Validator\Constraints as Assert;

#[ORM\Entity(repositoryClass: 'CpCreation\VitiCore\Cart\Repository\CartRepository')]
#[ORM\Table(name: 'cart_cart')]
#[ORM\Index(name: 'cart_cart_last_activity_at_idx', columns: ['last_activity_at'])]
#[ORM\Index(name: 'cart_cart_abandoned_at_idx', columns: ['abandoned_at'])]
#[ORM\HasLifecycleCallbacks]
class Cart implements HasTimestamp, HasPayment
{
    use SoftDeletable;
    use Timestamp;

    private const int EXPIRES_AFTER_DAYS = 3;
    private const int ACTIVITY_TOUCH_INTERVAL_SECONDS = 300;
    private const array PAYMENT_SUCCESS_VALUES = [
        '00000',
        '000',
        '00',
        '001',
        'authorized',
        'authorised',
        'complete',
        'in_progress',
        'no_payment_required',
        'ok',
        'paid',
        'paiement',
        'payetest',
        'payment',
        'payment_created',
        'succeeded',
        'success',
    ];
    private const array PAYMENT_FAILED_VALUES = [
        'declined',
        'error',
        'failed',
        'failure',
        'ko',
        'denied',
        'refused',
        'refuse',
        'refusé',
        'rejected',
        'requires_payment_method',
    ];
    public const string DOCUMENT_TYPE_ORDER = 'order';
    public const string DOCUMENT_TYPE_INVOICE = 'invoice';

    #[ORM\Id]
    #[ORM\Column(type: 'uuid')]
    #[ORM\GeneratedValue(strategy: 'NONE')]
    private UuidInterface $id;

    #[ORM\Column(type: 'text', nullable: true)]
    private ?string $token = null;

    #[ORM\Column(type: 'decimal', precision: 5, scale: 4, nullable: true)]
    private ?string $tax = null;

    #[ORM\Column(type: 'string', nullable: true)]
    private ?string $groupCode = null;

    /**
     * true = affichage HT
     * false/null = affichage TTC.
     */
    #[ORM\Column(type: 'boolean', nullable: true)]
    private ?bool $viewHT = null;

    #[ORM\Column(type: 'text', nullable: true)]
    private ?string $comment = null;

    #[ORM\Column(type: 'boolean', options: ['default' => false])]
    private bool $delivery = false;

    #[ORM\Column(type: 'datetime_immutable', nullable: true)]
    private ?\DateTimeImmutable $fulfilledAt = null;

    #[ORM\Column(type: 'boolean', options: ['default' => false])]
    #[Assert\IsTrue(message: 'shop.accept_cgv')]
    private bool $cgv = false;

    #[ORM\Column(type: 'string', length: 10, options: ['default' => 'fr'])]
    private string $locale = 'fr';

    #[ORM\Column(type: 'string', nullable: true)]
    private ?string $codeCarrier = null;

    #[ORM\Column(type: 'string', length: 64, nullable: true)]
    private ?string $trackingCarrier = null;

    #[ORM\Column(type: 'string', nullable: true)]
    private ?string $urlDeSuiviCarrier = null;

    #[ORM\Column(type: 'string', nullable: true)]
    private ?string $ipClient = null;

    #[ORM\ManyToOne(targetEntity: DeliveryMethod::class)]
    #[ORM\JoinColumn(referencedColumnName: 'id', nullable: true)]
    private ?DeliveryMethod $deliveryMethod = null;

    #[ORM\OneToOne(targetEntity: CartCheckout::class, mappedBy: 'cart', cascade: ['persist'])]
    #[Assert\Valid]
    private ?CartCheckout $cartCheckout = null;

    #[ORM\OneToOne(targetEntity: InvoiceAddress::class, cascade: ['persist'])]
    #[Assert\Valid(groups: ['invoice_address'])]
    private ?InvoiceAddress $invoiceAddress = null;

    #[ORM\OneToOne(targetEntity: DeliveryAddress::class, cascade: ['persist'])]
    #[Assert\NotBlank(groups: ['delivery_address'])]
    #[Assert\Valid(groups: ['delivery_address'])]
    private ?DeliveryAddress $deliveryAddress = null;

    #[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'carts')]
    #[ORM\JoinColumn(nullable: true, onDelete: 'SET NULL')]
    private ?User $user = null;

    #[ORM\OneToOne(inversedBy: 'cart', targetEntity: Invoice::class, cascade: ['all'])]
    private ?Invoice $invoice = null;

    #[ORM\OneToOne(targetEntity: WithdrawalRequest::class, mappedBy: 'cart')]
    private ?WithdrawalRequest $withdrawalRequest = null;

    #[ORM\Column(type: 'string', length: 32, options: ['default' => self::DOCUMENT_TYPE_ORDER])]
    private string $documentType = self::DOCUMENT_TYPE_ORDER;

    #[ORM\Column(type: 'datetime_immutable')]
    private \DateTimeImmutable $expiresAt;

    #[ORM\Column(type: 'datetime_immutable', nullable: true)]
    private ?\DateTimeImmutable $lastActivityAt = null;

    #[ORM\Column(type: 'datetime_immutable', nullable: true)]
    private ?\DateTimeImmutable $abandonedAt = null;

    #[ORM\Column(type: 'json_document', nullable: true, options: ['jsonb' => true])]
    private ?CartPricing $cartPricing = null;

    #[ORM\Column(type: 'json_document', nullable: true, options: ['jsonb' => true])]
    private ?CartShipping $cartShipping = null;

    #[ORM\Column(type: 'json_document', nullable: true, options: ['jsonb' => true])]
    private ?CartPromos $cartPromos = null;

    #[ORM\Column(type: 'json', nullable: true)]
    private ?array $marketingAttribution = null;

    #[ORM\Column(type: 'string', nullable: true)]
    private ?string $promoCodeInput = null;

    #[ORM\Column(type: 'string', nullable: true)]
    private ?string $promoCodeStatus = null;

    #[ORM\Column(type: 'string', nullable: true)]
    private ?string $promoCodeMessageKey = null;

    #[ORM\OneToMany(
        targetEntity: CartItem::class,
        mappedBy: 'cart',
        cascade: ['persist', 'remove'],
        orphanRemoval: true
    )]
    #[ORM\OrderBy(['createdAt' => 'ASC'])]
    #[Assert\Valid(groups: ['add_to_cart'])]
    /**
     * @var Collection<int, CartItem>
     */
    private Collection $items;

    public function __construct()
    {
        $this->id = Uuid::uuid4();
        $this->items = new ArrayCollection();
        $this->token = mb_strtoupper(bin2hex(random_bytes(3)));
        $this->expiresAt = new \DateTimeImmutable('+'.self::EXPIRES_AFTER_DAYS.' days');
        $this->lastActivityAt = new \DateTimeImmutable();
    }

    public function getId(): UuidInterface
    {
        return $this->id;
    }

    public function getToken(): ?string
    {
        return $this->token;
    }

    public function setToken(?string $token): void
    {
        $this->token = $token;
    }

    public function getCartShipping(): ?CartShipping
    {
        return $this->cartShipping;
    }

    public function setCartShipping(?CartShipping $cartShipping): void
    {
        if ($this->hasFrozenCommerceSnapshot()) {
            return;
        }

        if ($this->cartShipping === $cartShipping) {
            return;
        }

        $this->cartShipping = $cartShipping;
    }

    public function getShippingTTC(): ?int
    {
        return $this->cartShipping?->amountTTC;
    }

    public function getShippingTaxRate(): ?string
    {
        return null !== $this->cartShipping ? (string) $this->cartShipping->taxRate : null;
    }

    public function getShippingName(): ?string
    {
        return $this->cartShipping?->name;
    }

    public function getTax(): ?string
    {
        return $this->tax;
    }

    public function setTax(?string $tax): void
    {
        if ($this->tax === $tax) {
            return;
        }

        $this->tax = $tax;
    }

    public function getGroupCode(): ?string
    {
        return $this->groupCode;
    }

    public function setGroupCode(?string $groupCode): void
    {
        $this->groupCode = $groupCode;
    }

    public function getCartPromos(): ?CartPromos
    {
        return $this->cartPromos;
    }

    public function getPromoCodeInput(): ?string
    {
        return $this->promoCodeInput;
    }

    public function setPromoCodeInput(?string $promoCodeInput): void
    {
        if ($this->hasFrozenCommerceSnapshot()) {
            return;
        }

        $promoCodeInput = null !== $promoCodeInput ? mb_trim($promoCodeInput) : null;
        $promoCodeInput = '' === $promoCodeInput ? null : $promoCodeInput;

        if ($this->promoCodeInput === $promoCodeInput) {
            return;
        }

        $this->promoCodeInput = $promoCodeInput;
        $this->cartPromos = null;
    }

    public function getPromo(): ?string
    {
        return $this->getPromoCodeInput();
    }

    public function setPromo(?string $promo): void
    {
        $this->setPromoCodeInput($promo);
    }

    /**
     * @return list<CartPromo>
     */
    public function getPromos(): array
    {
        return $this->cartPromos?->items ?? [];
    }

    public function hasExistingDiscount(): bool
    {
        $pricing = $this->getPricing();
        if (null !== $pricing && ($pricing->discountTTC > 0 || $pricing->degressiveTTC > 0)) {
            return true;
        }

        foreach ($this->getPromos() as $promo) {
            if ($promo->getBenefitTotalTTC() > 0) {
                return true;
            }
        }

        return CartPromo::STATUS_APPLIED === $this->promoCodeStatus;
    }

    public function setCartPromos(?CartPromos $cartPromos): void
    {
        if ($this->hasFrozenCommerceSnapshot()) {
            return;
        }

        $this->cartPromos = $cartPromos;
    }

    public function addPromo(CartPromo $promo): void
    {
        if ($this->hasFrozenCommerceSnapshot()) {
            return;
        }

        $cartPromos = $this->cartPromos ?? new CartPromos();
        $cartPromos->add($promo);
        $this->cartPromos = $cartPromos;
    }

    public function clearPromos(): void
    {
        if ($this->hasFrozenCommerceSnapshot()) {
            return;
        }

        $this->cartPromos = null;
    }

    public function clearPricing(): void
    {
        if ($this->hasFrozenCommerceSnapshot()) {
            return;
        }

        $this->cartPricing = null;
    }

    public function clearShipping(): void
    {
        if ($this->hasFrozenCommerceSnapshot()) {
            return;
        }

        $this->cartShipping = null;
    }

    public function clearPromoCodeInput(): void
    {
        $this->setPromoCodeInput(null);
    }

    public function getPromoCodeStatus(): ?string
    {
        return $this->promoCodeStatus;
    }

    public function setPromoCodeStatus(?string $promoCodeStatus): void
    {
        $this->promoCodeStatus = $promoCodeStatus;
    }

    public function getPromoCodeMessageKey(): ?string
    {
        return $this->promoCodeMessageKey;
    }

    public function setPromoCodeMessageKey(?string $promoCodeMessageKey): void
    {
        $this->promoCodeMessageKey = $promoCodeMessageKey;
    }

    public function clearPromoCodeFeedback(): void
    {
        $this->promoCodeStatus = null;
        $this->promoCodeMessageKey = null;
    }

    public function getViewHT(): ?bool
    {
        return $this->viewHT;
    }

    public function setViewHT(?bool $viewHT): void
    {
        $this->viewHT = $viewHT;
    }

    public function getComment(): ?string
    {
        return $this->comment;
    }

    public function setComment(?string $comment): void
    {
        $this->comment = $comment;
    }

    public function isDelivery(): bool
    {
        return $this->delivery;
    }

    public function setDelivery(bool $delivery): void
    {
        if ($delivery) {
            $this->markFulfilled();

            return;
        }

        $this->clearFulfillment();
    }

    public function getFulfilledAt(): ?\DateTimeImmutable
    {
        return $this->fulfilledAt;
    }

    public function markFulfilled(?\DateTimeImmutable $fulfilledAt = null): void
    {
        $this->delivery = true;
        $this->fulfilledAt ??= $fulfilledAt ?? new \DateTimeImmutable();
    }

    public function clearFulfillment(): void
    {
        $this->delivery = false;
        $this->fulfilledAt = null;
    }

    public function isCgv(): bool
    {
        return $this->cgv;
    }

    public function getCgv(): bool
    {
        return $this->cgv;
    }

    public function setCgv(bool $cgv): void
    {
        $this->cgv = $cgv;
    }

    public function getLocale(): string
    {
        return $this->locale;
    }

    public function setLocale(string $locale): void
    {
        $locale = mb_trim($locale);
        $this->locale = '' !== $locale ? $locale : 'fr';
    }

    public function getCodeCarrier(): ?string
    {
        return $this->codeCarrier;
    }

    public function setCodeCarrier(?string $codeCarrier): void
    {
        $this->codeCarrier = $codeCarrier;
    }

    public function getTrackingCarrier(): ?string
    {
        return $this->trackingCarrier;
    }

    public function setTrackingCarrier(?string $trackingCarrier): void
    {
        $this->trackingCarrier = $trackingCarrier;
    }

    public function getUrlDeSuiviCarrier(): ?string
    {
        return $this->urlDeSuiviCarrier;
    }

    public function setUrlDeSuiviCarrier(?string $urlDeSuiviCarrier): void
    {
        $this->urlDeSuiviCarrier = $urlDeSuiviCarrier;
    }

    public function getIpClient(): ?string
    {
        return $this->ipClient;
    }

    public function setIpClient(?string $ipClient): void
    {
        $this->ipClient = $ipClient;
    }

    public function getDeliveryMethod(): ?DeliveryMethod
    {
        return $this->deliveryMethod;
    }

    public function setDeliveryMethod(?DeliveryMethod $deliveryMethod): void
    {
        $this->deliveryMethod = $deliveryMethod;
    }

    public function getCartCheckout(): ?CartCheckout
    {
        return $this->cartCheckout;
    }

    public function setCartCheckout(?CartCheckout $cartCheckout): void
    {
        $this->cartCheckout = $cartCheckout;
    }

    public function isCheckoutPayed(): bool
    {
        return null !== $this->cartCheckout?->getPayedAt();
    }

    public function isLegacyOrderImport(): bool
    {
        $metadata = $this->cartCheckout?->getResponse()['legacy_order_import'] ?? null;

        return \is_array($metadata) && 'viti-core-master' === ($metadata['source'] ?? null);
    }

    public function hasFrozenCommerceSnapshot(): bool
    {
        return $this->isCheckoutPayed() || $this->isCheckoutOrdered() || $this->isLegacyOrderImport();
    }

    public function isCheckoutInitiated(): bool
    {
        return null !== $this->cartCheckout?->getInitiatedAt();
    }

    public function isCheckoutOrdered(): bool
    {
        return null !== $this->cartCheckout?->getOrderedAt();
    }

    public function getCheckoutStatus(): string
    {
        if ($this->isCheckoutPayed()) {
            return 'paid';
        }

        if ('canceled' === $this->getPaymentReturnStatus()) {
            return 'canceled';
        }

        if ('failed' === $this->getPaymentReturnStatus()) {
            return 'failed';
        }

        if ($this->isCheckoutOrdered()) {
            return 'ordered';
        }

        if ($this->isAbandoned()) {
            return 'abandoned';
        }

        if ($this->isCheckoutInitiated()) {
            return 'initiated';
        }

        return 'pending';
    }

    public function getPaymentReturnStatus(): ?string
    {
        if ($this->isCheckoutPayed()) {
            return null;
        }

        $response = $this->getPaymentResponse();
        if ([] === $response) {
            return null;
        }

        $explicitStatus = self::findFirstResponsePathValue($response, [
            ['payment_return', 'status'],
            ['return_status'],
            ['status_code'],
        ]);
        $normalizedStatus = self::normalizePaymentStatus($explicitStatus);

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

        if (self::containsCanceledPaymentValue($response)) {
            return 'canceled';
        }

        if (null !== $this->getPaymentFailureCode() || null !== $this->getPaymentFailureMessage()) {
            return 'failed';
        }

        if (self::containsFailedPaymentValue($response)) {
            return 'failed';
        }

        return null;
    }

    public function getPaymentFailureCode(): ?string
    {
        return self::normalizePaymentCode(self::findFirstResponsePathValue($this->getPaymentResponse(), [
            ['payment_return', 'error_code'],
            ['Erreur'],
            ['code-retour'],
            ['vads_payment_error'],
            ['vads_result'],
            ['vads_auth_result'],
            ['vads_extra_result'],
            ['responseCode'],
            ['acquirerResponseCode'],
            ['Code'],
            ['code'],
            ['error_code'],
            ['failure_code'],
            ['failure', 'code'],
            ['last_payment_error', 'code'],
            ['stripe_checkout_sync', 'last_payment_error', 'code'],
            ['object', 'payment_intent', 'last_payment_error', 'code'],
            ['session', 'payment_intent', 'last_payment_error', 'code'],
        ]));
    }

    public function getPaymentFailureMessage(): ?string
    {
        return self::normalizePaymentMessage(self::findFirstResponsePathValue($this->getPaymentResponse(), [
            ['payment_return', 'reason'],
            ['motifrefus'],
            ['reason'],
            ['error_description'],
            ['error_message'],
            ['vads_error_message'],
            ['message'],
            ['error'],
            ['failure', 'message'],
            ['last_payment_error', 'message'],
            ['stripe_checkout_sync', 'last_payment_error', 'message'],
            ['object', 'payment_intent', 'last_payment_error', 'message'],
            ['session', 'payment_intent', 'last_payment_error', 'message'],
        ]));
    }

    /**
     * @return array<string, mixed>
     */
    private function getPaymentResponse(): array
    {
        $response = $this->cartCheckout?->getResponse();

        return \is_array($response) ? $response : [];
    }

    /**
     * @param array<string, mixed> $response
     * @param list<list<string>>   $paths
     */
    private static function findFirstResponsePathValue(array $response, array $paths): mixed
    {
        foreach ($paths as $path) {
            $value = self::findResponsePathValue($response, $path);

            if (null !== $value && '' !== $value) {
                return $value;
            }
        }

        return null;
    }

    /**
     * @param array<string, mixed> $response
     * @param list<string>         $path
     */
    private static function findResponsePathValue(array $response, array $path): mixed
    {
        $value = $response;

        foreach ($path as $key) {
            if (!\is_array($value) || !\array_key_exists($key, $value)) {
                return null;
            }

            $value = $value[$key];
        }

        return $value;
    }

    private static function normalizePaymentStatus(mixed $value): ?string
    {
        $value = self::normalizePaymentScalar($value);

        if (null === $value || self::isSuccessPaymentValue($value)) {
            return null;
        }

        if (self::isCanceledPaymentValue($value)) {
            return 'canceled';
        }

        if (self::isFailedPaymentValue($value)) {
            return 'failed';
        }

        return null;
    }

    private static function normalizePaymentCode(mixed $value): ?string
    {
        $value = self::normalizePaymentScalar($value);

        if (null === $value || self::isSuccessPaymentValue($value) || self::isCanceledPaymentValue($value) || self::isFailedPaymentValue($value)) {
            return null;
        }

        return mb_strlen($value) <= 120 ? $value : null;
    }

    private static function normalizePaymentMessage(mixed $value): ?string
    {
        $value = self::normalizePaymentScalar($value);

        if (null === $value || self::isSuccessPaymentValue($value) || self::isCanceledPaymentValue($value)) {
            return null;
        }

        return $value;
    }

    private static function normalizePaymentScalar(mixed $value): ?string
    {
        if (\is_array($value) || null === $value) {
            return null;
        }

        $value = preg_replace('/\s+/', ' ', mb_trim(strip_tags((string) $value))) ?? '';

        return '' !== $value ? $value : null;
    }

    private static function containsCanceledPaymentValue(mixed $value): bool
    {
        if (\is_array($value)) {
            foreach ($value as $item) {
                if (self::containsCanceledPaymentValue($item)) {
                    return true;
                }
            }

            return false;
        }

        $value = self::normalizePaymentScalar($value);

        return null !== $value && self::isCanceledPaymentValue($value);
    }

    private static function containsFailedPaymentValue(mixed $value): bool
    {
        if (\is_array($value)) {
            foreach ($value as $item) {
                if (self::containsFailedPaymentValue($item)) {
                    return true;
                }
            }

            return false;
        }

        $value = self::normalizePaymentScalar($value);

        return null !== $value && self::isFailedPaymentValue($value);
    }

    private static function isSuccessPaymentValue(string $value): bool
    {
        return \in_array(mb_strtolower($value), self::PAYMENT_SUCCESS_VALUES, true);
    }

    private static function isCanceledPaymentValue(string $value): bool
    {
        return 1 === preg_match('/annul|cancel|aband|expired/i', $value);
    }

    private static function isFailedPaymentValue(string $value): bool
    {
        return \in_array(mb_strtolower($value), self::PAYMENT_FAILED_VALUES, true);
    }

    public function addItem(CartItem $item): void
    {
        foreach ($this->items as $existingItem) {
            if (
                $existingItem->getVariant() === $item->getVariant()
                && $existingItem->hasSameOptionConfigurationAs($item)
            ) {
                $existingItem->setQuantity($existingItem->getQuantity() + $item->getQuantity());
                $this->touchActivity();

                return;
            }
        }

        $item->setCart($this);
        $this->items->add($item);
        $this->touchActivity();
    }

    public function getItems(): Collection
    {
        return $this->items;
    }

    public function getPricing(): ?CartPricing
    {
        return $this->cartPricing;
    }

    public function setPricing(?CartPricing $cartPricing): void
    {
        if ($this->hasFrozenCommerceSnapshot()) {
            return;
        }

        $this->cartPricing = $cartPricing;
    }

    /**
     * @return array<string, mixed>|null
     */
    public function getMarketingAttribution(): ?array
    {
        return $this->marketingAttribution;
    }

    /**
     * @param array<string, mixed>|null $marketingAttribution
     */
    public function setMarketingAttribution(?array $marketingAttribution): void
    {
        if ($this->isCheckoutPayed()) {
            return;
        }

        $this->marketingAttribution = $marketingAttribution;
    }

    public function getQuantityAllAbsolute(): int|float
    {
        $quantityAll = 0;

        foreach ($this->getItems() as $item) {
            $quantityAll += $item->getQuantity() * $this->resolveItemEquivalency($item);
        }

        return $quantityAll;
    }

    /**
     * @param Collection<int, Product> $products
     */
    public function getQuantityByProducts(Collection $products): int|float
    {
        if (0 === $products->count()) {
            return $this->getQuantityAllAbsolute();
        }

        $quantity = 0;

        foreach ($this->getItems() as $item) {
            foreach ($products as $selectedProduct) {
                if ($item->getVariant()?->getProduct() === $selectedProduct) {
                    $quantity += $item->getQuantity() * $this->resolveItemEquivalency($item);
                }
            }
        }

        return $quantity;
    }

    private function resolveItemEquivalency(CartItem $item): float
    {
        return $item->getVariant()?->getType()?->getEquivalency() ?? 0.0;
    }

    public function removeItem(CartItem $item): void
    {
        if (!$this->items->contains($item)) {
            return;
        }
        $this->items->removeElement($item);
        $item->setCart(null);
        $this->touchActivity();
    }

    public function removeEmptyItems(): void
    {
        foreach ($this->items->toArray() as $item) {
            if ($item->getQuantity() <= 0) {
                $this->removeItem($item);
            }
        }
    }

    public function getInvoiceAddress(): ?InvoiceAddress
    {
        return $this->invoiceAddress;
    }

    public function setInvoiceAddress(?InvoiceAddress $invoiceAddress): void
    {
        $this->invoiceAddress = $invoiceAddress;
    }

    public function normalizeInvoiceAddress(): void
    {
        if (null === $this->invoiceAddress) {
            return;
        }

        if (!$this->invoiceAddress->isDifferentAddress()) {
            $this->invoiceAddress = null;
        }
    }

    #[ORM\PrePersist]
    #[ORM\PreUpdate]
    public function syncInvoiceAddressBeforeSave(): void
    {
        $this->normalizeInvoiceAddress();
    }

    public function getDeliveryAddress(): ?DeliveryAddress
    {
        return $this->deliveryAddress;
    }

    public function setDeliveryAddress(?DeliveryAddress $deliveryAddress): void
    {
        $this->deliveryAddress = $deliveryAddress;
    }

    public function getUser(): ?User
    {
        return $this->user;
    }

    public function setUser(?User $user): void
    {
        if ($this->user === $user) {
            return;
        }

        $previousUser = $this->user;
        $this->user = $user;

        if (null !== $previousUser) {
            $previousUser->removeCart($this);
        }

        if (null !== $user && !$user->getCarts()->contains($this)) {
            $user->addCart($this);
        }
    }

    public function getInvoice(): ?Invoice
    {
        return $this->invoice;
    }

    public function setInvoice(?Invoice $invoice): void
    {
        $this->invoice = $invoice;
    }

    public function getDocumentType(): string
    {
        return match ($this->documentType) {
            self::DOCUMENT_TYPE_INVOICE => self::DOCUMENT_TYPE_INVOICE,
            default => self::DOCUMENT_TYPE_ORDER,
        };
    }

    public function setDocumentType(?string $documentType): void
    {
        if ($this->isCheckoutPayed()) {
            return;
        }

        $normalizedDocumentType = match ($documentType) {
            self::DOCUMENT_TYPE_INVOICE => self::DOCUMENT_TYPE_INVOICE,
            default => self::DOCUMENT_TYPE_ORDER,
        };

        if ($this->documentType === $normalizedDocumentType) {
            return;
        }

        $this->documentType = $normalizedDocumentType;
    }

    public function promoteToInvoiceDocument(): void
    {
        $this->documentType = self::DOCUMENT_TYPE_INVOICE;
    }

    public function isInvoiceDocumentRequested(): bool
    {
        return self::DOCUMENT_TYPE_INVOICE === $this->getDocumentType();
    }

    public function getDocumentLabelKey(): string
    {
        if (!$this->isInvoiceDocumentRequested()) {
            return 'checkout_design.document_order';
        }

        if ($this->isCheckoutPayed() || null !== $this->getInvoice()) {
            return 'checkout_design.document_invoice';
        }

        return 'checkout_design.document_proforma';
    }

    public function getDownloadDocumentLabelKey(): string
    {
        return match ($this->getDocumentLabelKey()) {
            'checkout_design.document_invoice' => 'checkout_design.download_invoice_pdf',
            'checkout_design.document_proforma' => 'checkout_design.download_proforma_pdf',
            default => 'checkout_design.download_order_pdf',
        };
    }

    public function getExpiresAt(): \DateTimeImmutable
    {
        return $this->expiresAt;
    }

    public function setExpiresAt(\DateTimeImmutable $expiresAt): void
    {
        $this->expiresAt = $expiresAt;
    }

    public function getLastActivityAt(): ?\DateTimeImmutable
    {
        return $this->lastActivityAt;
    }

    public function setLastActivityAt(?\DateTimeImmutable $lastActivityAt): void
    {
        $this->lastActivityAt = $lastActivityAt;
    }

    public function getAbandonedAt(): ?\DateTimeImmutable
    {
        return $this->abandonedAt;
    }

    public function setAbandonedAt(?\DateTimeImmutable $abandonedAt): void
    {
        if ($this->isCheckoutPayed() || $this->isCheckoutOrdered()) {
            return;
        }

        $this->abandonedAt = $abandonedAt;
    }

    public function markAbandoned(?\DateTimeImmutable $abandonedAt = null): bool
    {
        if ($this->isCheckoutPayed() || $this->isCheckoutOrdered() || null !== $this->abandonedAt) {
            return false;
        }

        $this->abandonedAt = $abandonedAt ?? new \DateTimeImmutable();

        return true;
    }

    public function clearAbandonment(): bool
    {
        if (null === $this->abandonedAt) {
            return false;
        }

        $this->abandonedAt = null;

        return true;
    }

    public function isAbandoned(): bool
    {
        return null !== $this->abandonedAt && !$this->isCheckoutPayed() && !$this->isCheckoutOrdered();
    }

    public function touchActivity(?\DateTimeImmutable $now = null): bool
    {
        if ($this->isCheckoutPayed() || $this->isCheckoutOrdered()) {
            return false;
        }

        $now ??= new \DateTimeImmutable();
        $wasAbandoned = $this->isAbandoned();
        $changed = $this->clearAbandonment();

        if (
            $wasAbandoned
            || null === $this->lastActivityAt
            || ($now->getTimestamp() - $this->lastActivityAt->getTimestamp()) >= self::ACTIVITY_TOUCH_INTERVAL_SECONDS
        ) {
            $this->lastActivityAt = $now;
            $changed = true;
        }

        return $changed;
    }

    public function getDegressiveValue(): ?int
    {
        return $this->getPricing()?->degressiveTTC;
    }

    public function getDegressiveValueTTC(): ?int
    {
        return $this->getPricing()?->degressiveTTC;
    }

    public function getEmail(): ?string
    {
        return $this->getInvoiceAddress()?->getEmail()
            ?? $this->getDeliveryAddress()?->getEmail()
            ?? $this->getUser()?->getEmail();
    }

    public function getWithdrawalRequest(): ?WithdrawalRequest
    {
        return $this->withdrawalRequest;
    }

    public function setWithdrawalRequest(?WithdrawalRequest $withdrawalRequest): void
    {
        $this->withdrawalRequest = $withdrawalRequest;
    }

    public function getPaymentMethod(): ?PaymentMethod
    {
        return $this->getCartCheckout()?->getPaymentMethod();
    }

    public function setPaymentMethod(?PaymentMethod $paymentMethod): void
    {
        if ($this->isCheckoutPayed()) {
            return;
        }

        $checkout = $this->getCartCheckout();

        if (null === $checkout) {
            $checkout = new CartCheckout();
            $checkout->setCart($this);
            $this->setCartCheckout($checkout);
        }

        $checkout->setPaymentMethod($paymentMethod);
    }

    public function getFirstname(): ?string
    {
        return $this->getBillingAddress()?->getFirstname();
    }

    public function getLastname(): ?string
    {
        return $this->getBillingAddress()?->getLastname();
    }

    public function getPhone(): ?string
    {
        return $this->getBillingAddress()?->getPhone();
    }

    public function getCompany(): ?string
    {
        return $this->getBillingAddress()?->getCompany();
    }

    public function getStreet(): ?string
    {
        return $this->getBillingAddress()?->getStreet();
    }

    public function getPostalCode(): ?string
    {
        return $this->getBillingAddress()?->getPostal();
    }

    public function getCity(): ?string
    {
        return $this->getBillingAddress()?->getCity();
    }

    public function getCountry(): ?string
    {
        return $this->getBillingAddress()?->getCountry();
    }

    private function getBillingAddress(): InvoiceAddress|DeliveryAddress|null
    {
        return $this->getInvoiceAddress() ?? $this->getDeliveryAddress();
    }

    public function getQuantity(): int
    {
        $quantity = 0;

        foreach ($this->getItems() as $item) {
            $quantity += $item->getQuantity();
        }

        return $quantity;
    }

    public function getTotal(): ?int
    {
        return $this->getPricing()?->totalTTC;
    }
}
