<?php
/**
 * For the full copyright and license information, please view the
 * docs/licenses/LICENSE.txt file that was distributed with this source code.
 */

namespace PrestaShop\PrestaShop\Core\Domain\Discount\Command;

use DateTimeImmutable;
use PrestaShop\Decimal\DecimalNumber;
use PrestaShop\PrestaShop\Core\Domain\Carrier\ValueObject\CarrierId;
use PrestaShop\PrestaShop\Core\Domain\Country\ValueObject\CountryId;
use PrestaShop\PrestaShop\Core\Domain\Currency\ValueObject\CurrencyId;
use PrestaShop\PrestaShop\Core\Domain\Customer\Group\ValueObject\GroupId;
use PrestaShop\PrestaShop\Core\Domain\Customer\ValueObject\CustomerId;
use PrestaShop\PrestaShop\Core\Domain\Customer\ValueObject\CustomerIdInterface;
use PrestaShop\PrestaShop\Core\Domain\Customer\ValueObject\NoCustomerId;
use PrestaShop\PrestaShop\Core\Domain\Discount\Exception\DiscountConstraintException;
use PrestaShop\PrestaShop\Core\Domain\Discount\ProductRuleGroup;
use PrestaShop\PrestaShop\Core\Domain\Discount\ValueObject\DiscountType;
use PrestaShop\PrestaShop\Core\Domain\Discount\ValueObject\MinimumAmount;
use PrestaShop\PrestaShop\Core\Domain\Language\ValueObject\LanguageId;
use PrestaShop\PrestaShop\Core\Domain\Product\Combination\Exception\CombinationConstraintException;
use PrestaShop\PrestaShop\Core\Domain\Product\Combination\ValueObject\CombinationId;
use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductConstraintException;
use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductId;
use PrestaShop\PrestaShop\Core\Domain\ValueObject\Money;

class AddDiscountCommand
{
    private array $localizedNames = [];
    private int $priority = 1;
    private bool $active = false;
    private ?DateTimeImmutable $validFrom = null;
    private ?DateTimeImmutable $validTo = null;
    private ?int $totalQuantity = null;
    private ?int $quantityPerUser = null;
    private string $description = '';
    private string $code = '';
    private ?CustomerIdInterface $customerId = null;
    private bool $highlightInCart = false;
    private bool $allowPartialUse = true;
    private DiscountType $type;
    private ?DecimalNumber $reductionPercent = null;
    private ?Money $reductionAmount = null;
    private ?ProductId $giftProductId = null;
    private ?CombinationId $giftCombinationId = null;
    private ?ProductId $reductionProductId = null;
    private bool $cheapestProduct = false;
    private ?int $minimumProductQuantity = null;
    /**
     * @var ProductRuleGroup[]|null
     */
    private ?array $productConditions = null;

    private ?MinimumAmount $minimumAmount = null;

    /**
     * @var CarrierId[]|null
     */
    private ?array $carrierIds = null;

    private ?array $countryIds = null;

    /**
     * @var GroupId[]|null
     */
    private ?array $customerGroupIds = null;
    /**
     * @var int[]|null
     */
    private ?array $compatibleDiscountTypeIds = null;

    public function __construct(
        string $type,
        array $localizedNames,
    ) {
        $this->type = new DiscountType($type);
        $this->localizedNames = $localizedNames;
    }

    /**
     * @param array<int, string> $localizedNames
     */
    public function setLocalizedNames(array $localizedNames): self
    {
        foreach ($localizedNames as $languageId => $name) {
            $this->localizedNames[(new LanguageId($languageId))->getValue()] = $name;
        }

        return $this;
    }

    /**
     * @return array<int, string>
     */
    public function getLocalizedNames(): array
    {
        return $this->localizedNames;
    }

    public function isActive(): bool
    {
        return $this->active;
    }

    public function setActive(bool $active): self
    {
        $this->active = $active;

        return $this;
    }

    public function getValidFrom(): ?DateTimeImmutable
    {
        return $this->validFrom;
    }

    public function getValidTo(): ?DateTimeImmutable
    {
        return $this->validTo;
    }

    /**
     * @throws DiscountConstraintException
     */
    public function setValidityDateRange(DateTimeImmutable $from, ?DateTimeImmutable $to = null): self
    {
        if ($to !== null) {
            $this->assertDateRangeIsValid($from, $to);
        }
        $this->validFrom = $from;
        $this->validTo = $to;

        return $this;
    }

    public function getPriority(): int
    {
        return $this->priority;
    }

    public function getDiscountType(): DiscountType
    {
        return $this->type;
    }

    /**
     * @throws DiscountConstraintException
     */
    public function setPriority(int $priority): self
    {
        if (0 >= $priority) {
            throw new DiscountConstraintException(
                sprintf('Invalid discount priority "%s". Must be a positive integer.', $priority),
                DiscountConstraintException::INVALID_PRIORITY
            );
        }

        $this->priority = $priority;

        return $this;
    }

    public function getTotalQuantity(): ?int
    {
        return $this->totalQuantity;
    }

    public function isHighlightInCart(): bool
    {
        return $this->highlightInCart;
    }

    public function setHighlightInCart(bool $highlightInCart): void
    {
        $this->highlightInCart = $highlightInCart;
    }

    /**
     * @throws DiscountConstraintException
     */
    public function setTotalQuantity(?int $quantity): self
    {
        if ($quantity !== null && 0 > $quantity) {
            throw new DiscountConstraintException(sprintf('Quantity cannot be lower than zero, %d given', $quantity), DiscountConstraintException::INVALID_QUANTITY);
        }

        $this->totalQuantity = $quantity;

        return $this;
    }

    public function getQuantityPerUser(): ?int
    {
        return $this->quantityPerUser;
    }

    /**
     * @throws DiscountConstraintException
     */
    public function setQuantityPerUser(?int $quantity): self
    {
        if ($quantity !== null && 0 > $quantity) {
            throw new DiscountConstraintException(sprintf('Quantity per user cannot be lower than zero, %d given', $quantity), DiscountConstraintException::INVALID_QUANTITY_PER_USER);
        }

        $this->quantityPerUser = $quantity;

        return $this;
    }

    public function getDescription(): string
    {
        return $this->description;
    }

    public function setDescription(string $description): self
    {
        $this->description = $description;

        return $this;
    }

    public function getCode(): string
    {
        return $this->code;
    }

    public function setCode(string $code): self
    {
        $this->code = $code;

        return $this;
    }

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

    public function setAllowPartialUse(bool $allow): self
    {
        $this->allowPartialUse = $allow;

        return $this;
    }

    public function getCustomerId(): ?CustomerIdInterface
    {
        return $this->customerId;
    }

    public function setCustomerId(int $customerId): self
    {
        $this->customerId = $customerId === NoCustomerId::NO_CUSTOMER_ID_VALUE ? new NoCustomerId() : new CustomerId($customerId);

        return $this;
    }

    public function getReductionPercent(): ?DecimalNumber
    {
        return $this->reductionPercent;
    }

    public function setReductionPercent(DecimalNumber $reductionPercent): self
    {
        $this->reductionPercent = $reductionPercent;

        return $this;
    }

    public function getReductionAmount(): ?Money
    {
        return $this->reductionAmount;
    }

    /**
     * Note: the parameters names are important here for API serialization.
     */
    public function setReductionAmount(DecimalNumber $amount, int $currencyId, bool $taxIncluded): self
    {
        if ($amount->isLowerThanZero()) {
            throw new DiscountConstraintException(sprintf('Money amount cannot be lower than zero, %s given', $amount), DiscountConstraintException::INVALID_DISCOUNT_VALUE_CANNOT_BE_NEGATIVE);
        }

        $this->reductionAmount = new Money($amount, new CurrencyId($currencyId), $taxIncluded);

        return $this;
    }

    public function getGiftProductId(): ?ProductId
    {
        return $this->giftProductId;
    }

    /**
     * @throws ProductConstraintException
     */
    public function setGiftProductId(?int $giftProductId): self
    {
        $this->giftProductId = null !== $giftProductId ? new ProductId($giftProductId) : null;

        return $this;
    }

    public function getGiftCombinationId(): ?CombinationId
    {
        return $this->giftCombinationId;
    }

    /**
     * @throws CombinationConstraintException
     */
    public function setGiftCombinationId(?int $giftCombinationId): self
    {
        $this->giftCombinationId = null !== $giftCombinationId ? new CombinationId($giftCombinationId) : null;

        return $this;
    }

    public function getCheapestProduct(): bool
    {
        return $this->cheapestProduct;
    }

    public function setCheapestProduct(bool $cheapestProduct): self
    {
        $this->cheapestProduct = $cheapestProduct;

        return $this;
    }

    public function getReductionProductId(): ?ProductId
    {
        return $this->reductionProductId;
    }

    /**
     * @throws ProductConstraintException
     */
    public function setReductionProductId(?int $reductionProductId): self
    {
        $this->reductionProductId = null !== $reductionProductId ? new ProductId($reductionProductId) : null;

        return $this;
    }

    public function getMinimumProductQuantity(): ?int
    {
        return $this->minimumProductQuantity;
    }

    public function setMinimumProductQuantity(int $minimumProductQuantity): self
    {
        if ($minimumProductQuantity < 0) {
            throw new DiscountConstraintException('Minimum products quantity must be greater than 0', DiscountConstraintException::INVALID_MINIMUM_PRODUCT_QUANTITY);
        }

        $this->minimumProductQuantity = $minimumProductQuantity;

        return $this;
    }

    public function getMinimumAmount(): ?MinimumAmount
    {
        return $this->minimumAmount;
    }

    /**
     * Note: the parameters names are important here for API serialization.
     * To unset the minimum amount set the first parameter to null (the other parameters can remain empty)
     */
    public function setMinimumAmount(?DecimalNumber $amount, int $currencyId = 0, bool $taxIncluded = true, bool $shippingIncluded = true): self
    {
        if (null === $amount) {
            $this->minimumAmount = null;
        } else {
            if ($amount->isLowerThanZero()) {
                throw new DiscountConstraintException(sprintf('Money amount cannot be lower than zero, %s given', $amount), DiscountConstraintException::INVALID_DISCOUNT_VALUE_CANNOT_BE_NEGATIVE);
            }

            $this->minimumAmount = new MinimumAmount($amount, new CurrencyId($currencyId), $taxIncluded, $shippingIncluded);
        }

        return $this;
    }

    /**
     * @return ProductRuleGroup[]|null
     */
    public function getProductConditions(): ?array
    {
        return $this->productConditions;
    }

    /**
     * @param ProductRuleGroup[]|array<int, array{quantity: int, rules: array<int, array{type: string, itemIds: int[]}>, type: string}> $productConditions
     *
     * @return self
     *
     * @throws DiscountConstraintException
     */
    public function setProductConditions(array $productConditions): self
    {
        $validProductConditions = [];
        foreach ($productConditions as $productCondition) {
            if (is_array($productCondition)) {
                $productCondition = ProductRuleGroup::fromArray($productCondition);
            }

            if (!$productCondition instanceof ProductRuleGroup) {
                throw new DiscountConstraintException(sprintf('Product conditions must be an array of %s', ProductRuleGroup::class), DiscountConstraintException::INVALID_PRODUCTS_CONDITIONS);
            }
            if ($productCondition->getQuantity() <= 0) {
                throw new DiscountConstraintException('Product conditions quantity must be strictly positive', DiscountConstraintException::INVALID_PRODUCTS_CONDITIONS);
            }
            if (empty($productCondition->getRules())) {
                throw new DiscountConstraintException('Product conditions rules cannot be empty', DiscountConstraintException::INVALID_PRODUCTS_CONDITIONS);
            }

            foreach ($productCondition->getRules() as $rule) {
                if (empty($rule->getItemIds())) {
                    throw new DiscountConstraintException('Product conditions rule items cannot be empty', DiscountConstraintException::INVALID_PRODUCTS_CONDITIONS);
                }

                foreach ($rule->getItemIds() as $itemId) {
                    if (!is_int($itemId)) {
                        throw new DiscountConstraintException('Product conditions rule item ID must be an integer', DiscountConstraintException::INVALID_PRODUCTS_CONDITIONS);
                    }
                    if ((int) $itemId <= 0) {
                        throw new DiscountConstraintException('Product conditions rule item ID must be strictly positive', DiscountConstraintException::INVALID_PRODUCTS_CONDITIONS);
                    }
                }
            }
            $validProductConditions[] = $productCondition;
        }
        $this->productConditions = $validProductConditions;

        return $this;
    }

    /**
     * @return CarrierId[]|null
     */
    public function getCarrierIds(): ?array
    {
        return $this->carrierIds;
    }

    /**
     * @param int[]|null $carrierIds
     *
     * @return $this
     */
    public function setCarrierIds(?array $carrierIds): self
    {
        $this->carrierIds = $carrierIds !== null ? array_map(fn (int $carrierId) => new CarrierId($carrierId), $carrierIds) : null;

        return $this;
    }

    public function getCountryIds(): ?array
    {
        return $this->countryIds;
    }

    public function setCountryIds(?array $countryIds): self
    {
        $this->countryIds = $countryIds !== null ? array_map(fn (int $countryId) => new CountryId($countryId), $countryIds) : $countryIds;

        return $this;
    }

    /**
     * @return GroupId[]|null
     */
    public function getCustomerGroupIds(): ?array
    {
        return $this->customerGroupIds;
    }

    /**
     * @return $this
     */
    public function setCustomerGroupIds(?array $customerGroupIds): self
    {
        $this->customerGroupIds = $customerGroupIds !== null ? array_map(fn (int $groupId) => new GroupId($groupId), $customerGroupIds) : null;

        return $this;
    }

    /**
     * @return int[]|null
     */
    public function getCompatibleDiscountTypeIds(): ?array
    {
        return $this->compatibleDiscountTypeIds;
    }

    /**
     * @param int[]|null $compatibleDiscountTypeIds
     */
    public function setCompatibleDiscountTypeIds(?array $compatibleDiscountTypeIds): self
    {
        foreach ($compatibleDiscountTypeIds as $compatibleDiscountTypeId) {
            if (!is_int($compatibleDiscountTypeId) || $compatibleDiscountTypeId <= 0) {
                throw new DiscountConstraintException('Compatible discount type ID must be positive integer', DiscountConstraintException::INVALID_COMPATIBLE_TYPE_IDS);
            }
        }
        $uniqueValues = array_unique($compatibleDiscountTypeIds);
        if ($uniqueValues !== $compatibleDiscountTypeIds) {
            throw new DiscountConstraintException('Provided compatible discount type ID must be unique', DiscountConstraintException::INVALID_COMPATIBLE_TYPE_IDS);
        }

        $this->compatibleDiscountTypeIds = $compatibleDiscountTypeIds;

        return $this;
    }

    /**
     * @throws DiscountConstraintException
     */
    private function assertDateRangeIsValid(DateTimeImmutable $dateFrom, DateTimeImmutable $dateTo): void
    {
        if ($dateFrom > $dateTo) {
            throw new DiscountConstraintException('Date from cannot be greater than date to.', DiscountConstraintException::DATE_FROM_GREATER_THAN_DATE_TO);
        }
    }
}
