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

use CpCreation\VitiCore\Bridge\Symfony\Bundle\Validator\Constraints\EffectiveSkuAvailable;
use CpCreation\VitiCore\Behavior\HasGroupAvailable;
use CpCreation\VitiCore\Behavior\HasTimestamp;
use CpCreation\VitiCore\Behavior\Impl\GroupAvailable;
use CpCreation\VitiCore\Behavior\Impl\SoftDeletable;
use CpCreation\VitiCore\Behavior\Impl\Timestamp;
use CpCreation\VitiCore\Behavior\IsSoftDeletable;
use CpCreation\VitiCore\Product\Repository\ProductVariantRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use Ramsey\Uuid\Uuid;
use Ramsey\Uuid\UuidInterface;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Webmozart\Assert\Assert as AssertDomain;

#[ORM\Entity(repositoryClass: ProductVariantRepository::class)]
#[ORM\Table(name: 'product_variant')]
#[ORM\UniqueConstraint(name: 'uniq_product_variant_sku', columns: ['sku'])]
#[UniqueEntity(fields: ['sku'], message: 'sku.variant.unique')]
#[EffectiveSkuAvailable]
#[ORM\HasLifecycleCallbacks]
class ProductVariant implements HasTimestamp, IsSoftDeletable, HasGroupAvailable
{
    use GroupAvailable;
    use SoftDeletable;
    use Timestamp;

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

    #[ORM\Column(type: 'integer')]
    #[Assert\NotBlank]
    #[Assert\GreaterThanOrEqual(
        value: 1,
        message: 'Le lot doit etre superieur ou egal a 1.',
    )]
    private ?int $lot = null;

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

    #[ORM\Column(type: 'boolean')]
    private bool $byDefault;

    #[ORM\Column(type: 'boolean')]
    private bool $unPublished;

    #[ORM\Column(type: 'boolean')]
    private bool $soldOut;

    #[ORM\ManyToOne(targetEntity: Product::class, inversedBy: 'variants')]
    #[ORM\JoinColumn(name: 'product_id', referencedColumnName: 'id')]
    #[Gedmo\SortableGroup]
    private ?Product $product = null;

    #[ORM\ManyToOne(targetEntity: ProductType::class)]
    #[ORM\JoinColumn(name: 'product_type_id', referencedColumnName: 'id')]
    private ?ProductType $type = null;

    #[ORM\Column(type: 'string', length: 255, nullable: true, unique: true)]
    #[Assert\Length(max: 255, maxMessage: 'sku.value_too_long')]
    private ?string $sku = null;

    /**
     * @var Collection<int, ProductPrice>
     */
    #[ORM\OneToMany(targetEntity: ProductPrice::class, mappedBy: 'variant', cascade: ['persist'])]
    #[Assert\Valid]
    private Collection $prices;

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

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

    /**
     * @var Collection<int, ProductVariantImage>
     */
    #[ORM\OneToMany(targetEntity: ProductVariantImage::class, mappedBy: 'variant', cascade: ['persist', 'remove'], orphanRemoval: true)]
    #[ORM\OrderBy(['position' => 'ASC'])]
    #[Assert\Valid]
    private Collection $images;

    /**
     * @var Collection<int, ProductVariantOption>
     */
    #[ORM\OneToMany(targetEntity: ProductVariantOption::class, mappedBy: 'variant', cascade: ['persist', 'remove'], orphanRemoval: true)]
    #[ORM\OrderBy(['position' => 'ASC'])]
    #[Assert\Valid]
    private Collection $options;

    public function __construct()
    {
        $this->id = Uuid::uuid4();
        $this->unPublished = false;
        $this->soldOut = false;
        $this->byDefault = false;
        $this->prices = new ArrayCollection();
        $this->images = new ArrayCollection();
        $this->options = new ArrayCollection();
    }

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

    public function getLot(): ?int
    {
        return $this->lot;
    }

    public function setLot(int $lot): void
    {
        $this->lot = $lot;
    }

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

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

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

    public function setByDefault(bool $byDefault): void
    {
        $this->byDefault = $byDefault;
    }

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

    public function setUnPublished(bool $unPublished): void
    {
        $this->unPublished = $unPublished;
    }

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

    public function setSoldOut(bool $soldOut): void
    {
        $this->soldOut = $soldOut;
    }

    public function getProduct(): ?Product
    {
        return $this->product;
    }

    public function setProduct(?Product $product): void
    {
        $this->product = $product;
    }

    public function getType(): ?ProductType
    {
        return $this->type;
    }

    public function setType(?ProductType $type): void
    {
        $this->type = $type;
    }

    public function getSku(): ?string
    {
        return $this->sku;
    }

    public function setSku(?string $sku): void
    {
        $this->sku = null !== $sku && '' !== trim($sku) ? trim($sku) : null;
    }

    public function getStockAt(): ?\DateTime
    {
        return $this->stockAt;
    }

    public function setStockAt(?\DateTime $stockAt): void
    {
        $this->stockAt = $stockAt;
    }

    public function getStock(): ?int
    {
        return $this->stock;
    }

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

    public function addPrice(ProductPrice $price): self
    {
        $price->setVariant($this);
        $this->prices[] = $price;

        return $this;
    }

    public function removePrice(ProductPrice $price): void
    {
        $this->prices->removeElement($price);
        $price->setVariant(null);
    }

    /**
     * @return Collection<int, ProductPrice>
     */
    public function getPrices(): Collection
    {
        return $this->prices;
    }

    public function addImage(ProductVariantImage $image): self
    {
        if (!$this->images->contains($image)) {
            $this->images->add($image);
            $image->setVariant($this);
        }

        return $this;
    }

    public function removeImage(ProductVariantImage $image): self
    {
        if ($this->images->removeElement($image) && $image->getVariant() === $this) {
            $image->setVariant(null);
        }

        return $this;
    }

    /**
     * @return Collection<int, ProductVariantImage>
     */
    public function getImages(): Collection
    {
        return $this->images;
    }

    public function addOption(ProductVariantOption $option): self
    {
        if (!$this->options->contains($option)) {
            $this->options->add($option);
            $option->setVariant($this);
        }

        return $this;
    }

    public function removeOption(ProductVariantOption $option): self
    {
        if ($this->options->removeElement($option) && $option->getVariant() === $this) {
            $option->setVariant(null);
        }

        return $this;
    }

    /**
     * @return Collection<int, ProductVariantOption>
     */
    public function getOptions(): Collection
    {
        return $this->options;
    }

    public function getName(): string
    {
        $quality = '';

        if ($this->getProduct()?->getQuality()) {
            $quality = $this->getProduct()->getQuality();
        }

        if (
            $this->getProduct()?->getVariants()->count() > 1
            || $this->getType()?->getName() !== $this->getProduct()?->getName()
        ) {
            return $this->getProduct()?->getName() . ' ' . $quality . ' - ' . $this->getType()?->getName();
        }

        return $this->getProduct()?->getName() . ' ' . $quality;
    }

    #[Assert\Callback]
    public function validate(ExecutionContextInterface $context): void
    {
        if ((null !== $this->getStock()) xor (null !== $this->getStockAt())) {
            $message = 'Vous devez entrer une date de début de stock si un stock est renseigné';

            $context->buildViolation($message)
                ->atPath('variant.stockAt')
                ->addViolation();
        }
    }

    public function getPriceByGroupId(mixed $groupId): ?ProductPrice
    {
        $groupId = $this->normalizeGroupId($groupId);
        if (null === $groupId) {
            return null;
        }

        foreach ($this->getPrices() as $price) {
            if ($price->getGroup()?->getId()?->toString() === $groupId) {
                return $price;
            }
        }

        return null;
    }

    private function normalizeGroupId(mixed $groupId): ?string
    {
        if ($groupId instanceof UuidInterface) {
            return $groupId->toString();
        }

        if ($groupId instanceof \Stringable) {
            $groupId = (string) $groupId;
        }

        if (!\is_string($groupId)) {
            return null;
        }

        $groupId = trim($groupId);

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

    public function getVariantPriceByGroupAvailable(): ?ProductPrice
    {
        if (null === $this->groupAvailable) {
            AssertDomain::null($this->groupAvailable, 'Group by default undefined');
        }

        $variantPrice = $this->prices->filter(function (ProductPrice $variantPrice) {
            return $variantPrice->getGroup() === $this->groupAvailable;
        })->reduce(function ($previous, ProductPrice $variantPrice) {
            return $variantPrice;
        });

        if (!$variantPrice) {
            $variantPrice = $this->prices->filter(function (ProductPrice $variantPrice) {
                return $variantPrice->getGroup()->isByDefault();
            })->reduce(function ($previous, ProductPrice $variantPrice) {
                return $variantPrice;
            });
        }

        return $variantPrice;
    }

    public function getLabel(): string
    {
        $product = $this->getProduct();
        $type = $this->getType();

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

        $unity = '';

        if ($type->getUnity()) {
            $unity = strip_tags((string) $type->getVolume()) . ' ' . $type->getUnity();
        }

        return mb_trim(strip_tags($type->getName()) . ' ' . $unity);
    }

    public function getFullName(): string
    {
        $product = $this->getProduct();
        $type = $this->getType();

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

        $unity = '';

        if ($type->getUnity()) {
            $unity = strip_tags((string) $type->getVolume()) . ' ' . $type->getUnity();
        }

        return mb_trim(strip_tags($product->getName() . ' ' . $type->getName()) . ' ' . $unity);
    }

    public function isAvailable(): bool
    {
        if ($this->isSoldOut()) {
            return false;
        }

        if (null !== $this->getStock() && $this->getStock() <= 0) {
            return false;
        }

        return true;
    }
}
