<?php

declare(strict_types=1);

/*
 * This file is part of the VitiCore 2.
 *
 * (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\HasDisable;
use CpCreation\VitiCore\Behavior\HasTimestamp;
use CpCreation\VitiCore\Behavior\Impl\Disable;
use CpCreation\VitiCore\Behavior\Impl\Timestamp;
use CpCreation\VitiCore\Bridge\Symfony\Bundle\Validator\Constraints\PromoTitleAvailability;
use CpCreation\VitiCore\Product\Model\Product;
use CpCreation\VitiCore\Product\Model\ProductPriceGroup;
use CpCreation\VitiCore\Product\Model\ProductVariant;
use CpCreation\VitiCore\Shop\Model\ShippingZone;
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;
use Symfony\Component\Validator\Context\ExecutionContextInterface;

#[ORM\Entity(repositoryClass: 'CpCreation\VitiCore\Cart\Repository\PromoRepository')]
#[ORM\Table(name: 'cart_promo')]
#[PromoTitleAvailability]
#[ORM\HasLifecycleCallbacks]
class Promo implements HasTimestamp, HasDisable
{
    use Disable;
    use Timestamp;

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

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

    /**
     * Pourcentage de remise.
     * Ex: 10.00 = 10%
     */
    #[ORM\Column(type: 'decimal', precision: 5, scale: 2, nullable: true)]
    private ?string $percent = null;

    /**
     * Montant fixe de remise TTC en centimes.
     */
    #[ORM\Column(type: 'integer', nullable: true)]
    private ?int $amountTTC = null;

    /**
     * Pourcentage de remise appliqué sur les frais de port.
     * Ex: 100.00 = livraison offerte.
     */
    #[ORM\Column(type: 'decimal', precision: 5, scale: 2, nullable: true)]
    private ?string $percentShippingCost = null;

    #[ORM\Column(type: 'datetime', nullable: true)]
    #[Assert\NotBlank]
    private ?\DateTimeInterface $beginAt = null;

    #[ORM\Column(type: 'datetime', nullable: true)]
    #[Assert\NotBlank]
    private ?\DateTimeInterface $endAt = null;

    #[ORM\Column(type: 'integer', options: ['default' => 0])]
    private int $count = 0;

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

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

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

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

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

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

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

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

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

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

    /**
     * @var Collection<int, ProductPriceGroup>
     */
    #[ORM\ManyToMany(targetEntity: ProductPriceGroup::class)]
    private Collection $groups;

    /**
     * @var Collection<int, ShippingZone>
     */
    #[ORM\ManyToMany(targetEntity: ShippingZone::class)]
    private Collection $zones;

    /**
     * @var Collection<int, Product>
     */
    #[ORM\ManyToMany(targetEntity: Product::class)]
    private Collection $products;

    /**
     * @var Collection<int, self>
     */
    #[ORM\ManyToMany(targetEntity: self::class, inversedBy: 'incompatibleAutoPromoOf')]
    #[ORM\JoinTable(name: 'cart_promo_incompatible_auto')]
    #[ORM\JoinColumn(name: 'promo_source_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
    #[ORM\InverseJoinColumn(name: 'promo_target_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
    private Collection $incompatibleAutoPromos;

    /**
     * @var Collection<int, self>
     */
    #[ORM\ManyToMany(targetEntity: self::class, mappedBy: 'incompatibleAutoPromos')]
    private Collection $incompatibleAutoPromoOf;

    #[ORM\ManyToOne(targetEntity: ProductVariant::class)]
    #[ORM\JoinColumn(name: 'offered_variant_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
    private ?ProductVariant $offeredVariant = null;

    #[ORM\ManyToOne(targetEntity: User::class)]
    #[ORM\JoinColumn(name: 'assigned_user_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')]
    private ?User $assignedUser = null;

    #[ORM\Column(type: 'string', length: 255, nullable: true)]
    #[Assert\Email]
    private ?string $notificationEmail = null;

    /**
     * Restriction exacte par codes pays ISO 3166-1 alpha-2.
     *
     * @var list<string>
     */
    #[ORM\Column(type: 'json', nullable: true, options: ['jsonb' => true])]
    private ?array $restrictedCountryCodes = null;

    public function __construct()
    {
        $this->id = Uuid::uuid4();
        $this->groups = new ArrayCollection();
        $this->products = new ArrayCollection();
        $this->zones = new ArrayCollection();
        $this->incompatibleAutoPromos = new ArrayCollection();
        $this->incompatibleAutoPromoOf = new ArrayCollection();
        $this->disabled = false;
        $this->auto = false;
        $this->multipleByQuantity = false;
        $this->includeShippingFees = false;
        $this->cumulative = false;
    }

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

    public function getTitle(): ?string
    {
        return $this->title;
    }

    public function setTitle(?string $title): void
    {
        $title = null !== $title ? mb_strtoupper(mb_trim($title)) : null;
        $this->title = '' === $title ? null : $title;
    }

    /**
     * Montant fixe de remise TTC en centimes.
     */
    public function getAmountTTC(): ?int
    {
        return $this->amountTTC;
    }

    /**
     * Montant fixe de remise TTC en centimes.
     */
    public function setAmountTTC(?int $amountTTC): void
    {
        $this->amountTTC = null !== $amountTTC ? max(0, $amountTTC) : null;
    }

    /**
     * Alias de compatibilité éventuelle.
     */
    public function getAmount(): ?int
    {
        return $this->getAmountTTC();
    }

    /**
     * Alias de compatibilité éventuelle.
     */
    public function setAmount(?int $amount): void
    {
        $this->setAmountTTC($amount);
    }

    public function getPercent(): ?float
    {
        return null !== $this->percent ? (float) $this->percent : null;
    }

    public function setPercent(?float $percent): void
    {
        $this->percent = null !== $percent ? (string) $percent : null;
    }

    public function getPercentShippingCost(): ?float
    {
        return null !== $this->percentShippingCost ? (float) $this->percentShippingCost : null;
    }

    public function setPercentShippingCost(?float $percentShippingCost): void
    {
        $this->percentShippingCost = null !== $percentShippingCost ? (string) $percentShippingCost : null;
    }

    public function getCount(): int
    {
        return $this->count;
    }

    public function setCount(int $count): void
    {
        $this->count = max(0, $count);
    }

    public function getMaxCount(): ?int
    {
        return $this->maxCount;
    }

    public function setMaxCount(?int $maxCount): void
    {
        $this->maxCount = null !== $maxCount ? max(0, $maxCount) : null;
    }

    public function getMaxCountByCustomer(): ?int
    {
        return $this->maxCountByCustomer;
    }

    public function setMaxCountByCustomer(?int $maxCountByCustomer): void
    {
        $this->maxCountByCustomer = null !== $maxCountByCustomer ? max(0, $maxCountByCustomer) : null;
    }

    public function isAuto(): bool
    {
        return $this->auto;
    }

    public function setAuto(bool $auto): void
    {
        $this->auto = $auto;
    }

    public function isMultipleByQuantity(): bool
    {
        return $this->multipleByQuantity;
    }

    public function setMultipleByQuantity(bool $multipleByQuantity): void
    {
        $this->multipleByQuantity = $multipleByQuantity;
    }

    public function isIncludeShippingFees(): bool
    {
        return $this->includeShippingFees;
    }

    public function setIncludeShippingFees(bool $includeShippingFees): void
    {
        $this->includeShippingFees = $includeShippingFees;
    }

    public function isCumulative(): bool
    {
        return $this->cumulative;
    }

    public function setCumulative(bool $cumulative): void
    {
        $this->cumulative = $cumulative;
    }

    public function getBeginAt(): ?\DateTimeInterface
    {
        return $this->beginAt;
    }

    public function setBeginAt(?\DateTimeInterface $beginAt): void
    {
        $this->beginAt = $beginAt;
    }

    public function getEndAt(): ?\DateTimeInterface
    {
        return $this->endAt;
    }

    public function setEndAt(?\DateTimeInterface $endAt): void
    {
        $this->endAt = $endAt;
    }

    public function getMinEquivalency(): ?int
    {
        return $this->minEquivalency;
    }

    public function setMinEquivalency(?int $minEquivalency): void
    {
        $this->minEquivalency = $minEquivalency;
    }

    public function getMaxEquivalency(): ?int
    {
        return $this->maxEquivalency;
    }

    public function setMaxEquivalency(?int $maxEquivalency): void
    {
        $this->maxEquivalency = $maxEquivalency;
    }

    public function getMinSubtotalTTC(): ?int
    {
        return $this->minSubtotalTTC;
    }

    public function setMinSubtotalTTC(?int $minSubtotalTTC): void
    {
        $this->minSubtotalTTC = null !== $minSubtotalTTC ? max(0, $minSubtotalTTC) : null;
    }

    public function getMaxSubtotalTTC(): ?int
    {
        return $this->maxSubtotalTTC;
    }

    public function setMaxSubtotalTTC(?int $maxSubtotalTTC): void
    {
        $this->maxSubtotalTTC = null !== $maxSubtotalTTC ? max(0, $maxSubtotalTTC) : null;
    }

    public function getOfferedVariant(): ?ProductVariant
    {
        return $this->offeredVariant;
    }

    public function setOfferedVariant(?ProductVariant $offeredVariant): void
    {
        $this->offeredVariant = $offeredVariant;
    }

    public function getAssignedUser(): ?User
    {
        return $this->assignedUser;
    }

    public function setAssignedUser(?User $assignedUser): void
    {
        $this->assignedUser = $assignedUser;
    }

    public function getNotificationEmail(): ?string
    {
        return $this->notificationEmail;
    }

    public function setNotificationEmail(?string $notificationEmail): void
    {
        $notificationEmail = null !== $notificationEmail ? mb_strtolower(mb_trim($notificationEmail)) : null;
        $this->notificationEmail = '' === $notificationEmail ? null : $notificationEmail;
    }

    public function getRecipientEmail(): ?string
    {
        return $this->getAssignedUser()?->getEmail() ?? $this->getNotificationEmail();
    }

    public function getEffectiveMaxCountByCustomer(): ?int
    {
        if (null !== $this->getAssignedUser() || null !== $this->getNotificationEmail()) {
            return 1;
        }

        return $this->getMaxCountByCustomer();
    }

    /**
     * @return list<string>
     */
    public function getRestrictedCountryCodes(): array
    {
        return $this->restrictedCountryCodes ?? [];
    }

    /**
     * @param iterable<mixed> $countryCodes
     */
    public function setRestrictedCountryCodes(iterable $countryCodes): void
    {
        $normalized = [];

        foreach ($countryCodes as $countryCode) {
            if (!\is_scalar($countryCode) && !$countryCode instanceof \Stringable) {
                continue;
            }

            $countryCode = mb_strtoupper(mb_trim((string) $countryCode));
            if (2 === mb_strlen($countryCode)) {
                $normalized[$countryCode] = true;
            }
        }

        $countryCodes = array_keys($normalized);
        sort($countryCodes);
        $this->restrictedCountryCodes = [] === $countryCodes ? null : $countryCodes;
    }

    /**
     * @return Collection<int, ProductPriceGroup>
     */
    public function getGroups(): Collection
    {
        return $this->groups;
    }

    public function addGroup(ProductPriceGroup $group): self
    {
        if (!$this->groups->contains($group)) {
            $this->groups->add($group);
        }

        return $this;
    }

    public function removeGroup(ProductPriceGroup $group): void
    {
        $this->groups->removeElement($group);
    }

    /**
     * @return Collection<int, ShippingZone>
     */
    public function getZones(): Collection
    {
        return $this->zones;
    }

    public function addZone(ShippingZone $zone): self
    {
        if (!$this->zones->contains($zone)) {
            $this->zones->add($zone);
        }

        return $this;
    }

    public function removeZone(ShippingZone $zone): void
    {
        $this->zones->removeElement($zone);
    }

    /**
     * @return Collection<int, Product>
     */
    public function getProducts(): Collection
    {
        return $this->products;
    }

    public function addProduct(Product $product): self
    {
        if (!$this->products->contains($product)) {
            $this->products->add($product);
        }

        return $this;
    }

    public function removeProduct(Product $product): void
    {
        $this->products->removeElement($product);
    }

    /**
     * @return Collection<int, self>
     */
    public function getIncompatibleAutoPromos(): Collection
    {
        return $this->incompatibleAutoPromos;
    }

    /**
     * @return Collection<int, self>
     */
    public function getIncompatibleAutoPromoOf(): Collection
    {
        return $this->incompatibleAutoPromoOf;
    }

    public function addIncompatibleAutoPromo(self $promo): self
    {
        if ($promo === $this || $this->incompatibleAutoPromos->contains($promo)) {
            return $this;
        }

        $this->incompatibleAutoPromos->add($promo);

        if (!$promo->incompatibleAutoPromoOf->contains($this)) {
            $promo->incompatibleAutoPromoOf->add($this);
        }

        return $this;
    }

    public function removeIncompatibleAutoPromo(self $promo): void
    {
        if (!$this->incompatibleAutoPromos->removeElement($promo)) {
            return;
        }

        $promo->incompatibleAutoPromoOf->removeElement($this);
    }

    public function isIncompatibleWithAutoPromo(self $promo): bool
    {
        if ($promo === $this) {
            return false;
        }

        return $this->incompatibleAutoPromos->contains($promo)
            || $this->incompatibleAutoPromoOf->contains($promo);
    }

    #[Assert\Callback]
    public function validate(ExecutionContextInterface $context): void
    {
        $hasPercent = null !== $this->getPercent();
        $hasAmountTTC = null !== $this->getAmountTTC();
        $hasPercentShippingCost = null !== $this->getPercentShippingCost();
        $hasOfferedVariant = null !== $this->getOfferedVariant();

        $selectedCount = (int) $hasPercent + (int) $hasAmountTTC + (int) $hasPercentShippingCost;

        if (0 === $selectedCount && !$hasOfferedVariant) {
            $context
                ->buildViolation('Veuillez entrer un pourcentage de remise, un montant de remise TTC ou un pourcentage sur les frais de port.')
                ->atPath('percent')
                ->addViolation();
        }

        if ($selectedCount > 1) {
            $context
                ->buildViolation('Veuillez entrer soit un pourcentage de remise, soit un montant de remise TTC, soit un pourcentage sur les frais de port, mais pas plusieurs à la fois.')
                ->atPath('percent')
                ->addViolation();
        }

        if (null !== $this->assignedUser && null !== $this->notificationEmail) {
            $context
                ->buildViolation('Veuillez associer soit un client enregistré, soit un email de notification, mais pas les deux.')
                ->atPath('assignedUser')
                ->addViolation();
        }

        if (null !== $this->notificationEmail && $this->isAuto()) {
            $context
                ->buildViolation('Une promo associée uniquement à un email de notification ne peut pas être automatique.')
                ->atPath('auto')
                ->addViolation();
        }

        if (
            null !== $this->minSubtotalTTC
            && null !== $this->maxSubtotalTTC
            && $this->maxSubtotalTTC < $this->minSubtotalTTC
        ) {
            $context
                ->buildViolation('promo.max_subtotal_ttc_lower_than_minimum')
                ->atPath('maxSubtotalTTC')
                ->addViolation();
        }

        if (!$this->isAuto() && !$this->incompatibleAutoPromos->isEmpty()) {
            $context
                ->buildViolation('Les incompatibilités ne sont disponibles que pour les promos automatiques.')
                ->atPath('incompatibleAutoPromos')
                ->addViolation();
        }

        foreach ($this->incompatibleAutoPromos as $promo) {
            if (!$promo->isAuto()) {
                $context
                    ->buildViolation('Seules des promos automatiques peuvent être marquées comme incompatibles.')
                    ->atPath('incompatibleAutoPromos')
                    ->addViolation();

                break;
            }
        }
    }
}
