<?php

declare(strict_types=1);

/*
 * 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\Marketing\Model;

use CpCreation\VitiCore\Behavior\HasTimestamp;
use CpCreation\VitiCore\Behavior\Impl\Timestamp;
use CpCreation\VitiCore\Marketing\Repository\AbandonedCartRuleRepository;
use CpCreation\VitiCore\Product\Model\ProductPriceGroup;
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: AbandonedCartRuleRepository::class)]
#[ORM\Table(name: 'marketing_abandoned_cart_rule')]
#[ORM\Index(name: 'marketing_abandoned_cart_rule_dispatch_idx', columns: ['enabled', 'position'])]
#[ORM\HasLifecycleCallbacks]
class AbandonedCartRule implements HasTimestamp
{
    use Timestamp;

    public const string CUSTOMER_ANY = 'any';
    public const string CUSTOMER_NEW = 'new';
    public const string CUSTOMER_RETURNING = 'returning';

    public const string DISCOUNT_NONE = 'none';
    public const string DISCOUNT_FIXED = 'fixed';
    public const string DISCOUNT_PERCENT = 'percent';
    public const string DISCOUNT_FREE_SHIPPING = 'free_shipping';

    public const array CUSTOMER_TYPES = [
        self::CUSTOMER_ANY,
        self::CUSTOMER_NEW,
        self::CUSTOMER_RETURNING,
    ];

    public const array DISCOUNT_TYPES = [
        self::DISCOUNT_NONE,
        self::DISCOUNT_FIXED,
        self::DISCOUNT_PERCENT,
        self::DISCOUNT_FREE_SHIPPING,
    ];

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

    #[ORM\ManyToOne(targetEntity: AbandonedCartConfiguration::class)]
    #[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
    private AbandonedCartConfiguration $configuration;

    #[Assert\NotBlank]
    #[Assert\Length(max: 120)]
    #[ORM\Column(type: 'string', length: 120)]
    private string $name = '';

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

    #[Assert\PositiveOrZero]
    #[ORM\Column(type: 'smallint', options: ['default' => 0])]
    private int $position = 0;

    #[Assert\Range(min: 1, max: 2160)]
    #[ORM\Column(type: 'smallint', options: ['default' => 24])]
    private int $delayHours = 24;

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

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

    /**
     * @var list<string>
     */
    #[ORM\Column(type: 'json', options: ['default' => '[]'])]
    private array $countryCodes = [];

    #[Assert\Choice(choices: self::CUSTOMER_TYPES)]
    #[ORM\Column(type: 'string', length: 16, options: ['default' => self::CUSTOMER_ANY])]
    private string $customerType = self::CUSTOMER_ANY;

    #[Assert\Choice(choices: self::DISCOUNT_TYPES)]
    #[ORM\Column(type: 'string', length: 24, options: ['default' => self::DISCOUNT_NONE])]
    private string $discountType = self::DISCOUNT_NONE;

    #[Assert\NotNull]
    #[ORM\ManyToOne(targetEntity: ProductPriceGroup::class)]
    #[ORM\JoinColumn(nullable: false)]
    private ?ProductPriceGroup $priceGroup = null;

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

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

    #[Assert\Positive]
    #[Assert\LessThanOrEqual(100)]
    #[ORM\Column(type: 'decimal', precision: 5, scale: 2, nullable: true)]
    private ?string $discountPercent = null;

    #[Assert\Range(min: 1, max: 90)]
    #[ORM\Column(type: 'smallint', options: ['default' => 7])]
    private int $validityDays = 7;

    public function __construct(AbandonedCartConfiguration $configuration)
    {
        $this->id = Uuid::uuid4();
        $this->configuration = $configuration;
    }

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

    public function getConfiguration(): AbandonedCartConfiguration
    {
        return $this->configuration;
    }

    public function getName(): string
    {
        return $this->name;
    }

    public function setName(string $name): void
    {
        $this->name = mb_trim($name);
    }

    public function isEnabled(): bool
    {
        return $this->enabled;
    }

    public function setEnabled(bool $enabled): void
    {
        $this->enabled = $enabled;
    }

    public function getPosition(): int
    {
        return $this->position;
    }

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

    public function getDelayHours(): int
    {
        return $this->delayHours;
    }

    public function setDelayHours(int $delayHours): void
    {
        $this->delayHours = $delayHours;
    }

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

    /**
     * @return list<string>
     */
    public function getCountryCodes(): array
    {
        return $this->countryCodes;
    }

    /**
     * @param iterable<mixed> $countryCodes
     */
    public function setCountryCodes(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;
            }
        }

        $this->countryCodes = array_keys($normalized);
        sort($this->countryCodes);
    }

    public function getCustomerType(): string
    {
        return $this->customerType;
    }

    public function setCustomerType(string $customerType): void
    {
        $this->customerType = \in_array($customerType, self::CUSTOMER_TYPES, true)
            ? $customerType
            : self::CUSTOMER_ANY;
    }

    public function getDiscountType(): string
    {
        return $this->discountType;
    }

    public function setDiscountType(string $discountType): void
    {
        $this->discountType = \in_array($discountType, self::DISCOUNT_TYPES, true)
            ? $discountType
            : self::DISCOUNT_NONE;
    }

    public function getPriceGroup(): ?ProductPriceGroup
    {
        return $this->priceGroup;
    }

    public function setPriceGroup(?ProductPriceGroup $priceGroup): void
    {
        $this->priceGroup = $priceGroup;
    }

    public function isCouponOnlyWithoutExistingDiscount(): bool
    {
        return $this->couponOnlyWithoutExistingDiscount;
    }

    public function setCouponOnlyWithoutExistingDiscount(bool $couponOnlyWithoutExistingDiscount): void
    {
        $this->couponOnlyWithoutExistingDiscount = $couponOnlyWithoutExistingDiscount;
    }

    public function getDiscountAmountTTC(): ?int
    {
        return $this->discountAmountTTC;
    }

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

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

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

    public function getValidityDays(): int
    {
        return $this->validityDays;
    }

    public function setValidityDays(int $validityDays): void
    {
        $this->validityDays = $validityDays;
    }

    public function createsCoupon(): bool
    {
        return self::DISCOUNT_NONE !== $this->discountType;
    }

    #[Assert\Callback]
    public function validate(ExecutionContextInterface $context): void
    {
        if (
            null !== $this->minSubtotalTTC
            && null !== $this->maxSubtotalTTC
            && $this->maxSubtotalTTC < $this->minSubtotalTTC
        ) {
            $context
                ->buildViolation('marketing.abandoned_cart.validation.max_amount')
                ->atPath('maxSubtotalTTC')
                ->addViolation();
        }

        if (self::DISCOUNT_FIXED === $this->discountType && (null === $this->discountAmountTTC || $this->discountAmountTTC <= 0)) {
            $context
                ->buildViolation('marketing.abandoned_cart.validation.discount_value_required')
                ->atPath('discountAmountTTC')
                ->addViolation();
        }

        if (self::DISCOUNT_PERCENT === $this->discountType && (null === $this->discountPercent || $this->discountPercent <= 0)) {
            $context
                ->buildViolation('marketing.abandoned_cart.validation.discount_value_required')
                ->atPath('discountPercent')
                ->addViolation();
        }
    }
}
