<?php

declare(strict_types=1);

namespace App\Entity;

use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Delete;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Put;
use App\Entity\Trait\CuidEntityTrait;
use App\Enum\ModelCategory;
use App\Repository\ModelTypeRepository;
use App\State\ModelTypeProcessor;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;

#[ORM\Entity(repositoryClass: ModelTypeRepository::class)]
#[ORM\Table(name: 'model_types')]
#[ORM\UniqueConstraint(name: 'unique_category_name', columns: ['category', 'name'])]
#[ORM\HasLifecycleCallbacks]
#[ApiFilter(SearchFilter::class, properties: ['category' => 'exact', 'name' => 'ipartial'])]
#[ApiFilter(OrderFilter::class, properties: ['name', 'createdAt'])]
#[ApiResource(
    description: 'Types et catégories. Référentiel de classification pour les machines, pièces, composants et produits. Chaque type appartient à une catégorie (machine, piece, composant, product) et peut être converti.',
    operations: [
        new Get(security: "is_granted('ROLE_VIEWER')"),
        new GetCollection(security: "is_granted('ROLE_VIEWER')"),
        new Post(security: "is_granted('ROLE_GESTIONNAIRE')", processor: ModelTypeProcessor::class),
        new Put(security: "is_granted('ROLE_GESTIONNAIRE')", processor: ModelTypeProcessor::class),
        new Patch(security: "is_granted('ROLE_GESTIONNAIRE')", processor: ModelTypeProcessor::class),
        new Delete(security: "is_granted('ROLE_GESTIONNAIRE')"),
    ],
    paginationClientItemsPerPage: true,
    paginationMaximumItemsPerPage: 200
)]
class ModelType
{
    use CuidEntityTrait;

    #[ORM\Id]
    #[ORM\Column(type: Types::STRING, length: 36)]
    #[Groups(['type_machine:read', 'model_type:read', 'product:read', 'composant:read', 'piece:read'])]
    private ?string $id = null;

    #[ORM\Column(type: Types::STRING, length: 120)]
    #[Groups(['type_machine:read', 'model_type:read', 'model_type:write', 'product:read', 'composant:read', 'piece:read'])]
    private string $name;

    #[ORM\Column(type: Types::STRING, length: 60, unique: true)]
    #[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
    private string $code;

    #[ORM\Column(enumType: ModelCategory::class)]
    #[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
    private ModelCategory $category;

    #[ORM\Column(type: Types::TEXT, nullable: true)]
    #[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
    private ?string $notes = null;

    #[ORM\Column(type: Types::TEXT, nullable: true)]
    #[Groups(['type_machine:read', 'model_type:read', 'model_type:write'])]
    private ?string $description = null;

    #[ORM\Column(type: Types::TEXT, nullable: true)]
    #[Groups(['model_type:read', 'model_type:write'])]
    private ?string $referenceFormula = null;

    #[ORM\Column(type: Types::JSON, nullable: true)]
    #[Groups(['model_type:read', 'model_type:write'])]
    private ?array $requiredFieldsForReference = null;

    #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'createdAt')]
    #[Groups(['model_type:read'])]
    private DateTimeImmutable $createdAt;

    #[ORM\Column(type: Types::DATETIME_IMMUTABLE, name: 'updatedAt')]
    #[Groups(['model_type:read'])]
    private DateTimeImmutable $updatedAt;

    private ?array $pendingStructure = null;

    /**
     * @var Collection<int, Composant>
     */
    #[ORM\OneToMany(mappedBy: 'typeComposant', targetEntity: Composant::class)]
    private Collection $composants;

    /**
     * @var Collection<int, Piece>
     */
    #[ORM\OneToMany(mappedBy: 'typePiece', targetEntity: Piece::class)]
    private Collection $pieces;

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

    /**
     * @var Collection<int, CustomField>
     */
    #[ORM\OneToMany(mappedBy: 'typeComposant', targetEntity: CustomField::class)]
    private Collection $customFields;

    /**
     * @var Collection<int, CustomField>
     */
    #[ORM\OneToMany(mappedBy: 'typePiece', targetEntity: CustomField::class)]
    private Collection $pieceCustomFields;

    /**
     * @var Collection<int, CustomField>
     */
    #[ORM\OneToMany(mappedBy: 'typeProduct', targetEntity: CustomField::class)]
    private Collection $productCustomFields;

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

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

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

    public function __construct()
    {
        $this->createdAt                        = new DateTimeImmutable();
        $this->updatedAt                        = new DateTimeImmutable();
        $this->composants                       = new ArrayCollection();
        $this->pieces                           = new ArrayCollection();
        $this->products                         = new ArrayCollection();
        $this->customFields                     = new ArrayCollection();
        $this->pieceCustomFields                = new ArrayCollection();
        $this->productCustomFields              = new ArrayCollection();
        $this->skeletonPieceRequirements        = new ArrayCollection();
        $this->skeletonProductRequirements      = new ArrayCollection();
        $this->skeletonSubcomponentRequirements = new ArrayCollection();
    }

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

    public function setName(string $name): static
    {
        $this->name = mb_strtoupper(mb_substr($name, 0, 1)).mb_substr($name, 1);

        return $this;
    }

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

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

        return $this;
    }

    public function getCategory(): ModelCategory
    {
        return $this->category;
    }

    public function setCategory(ModelCategory $category): static
    {
        $this->category = $category;

        return $this;
    }

    public function getNotes(): ?string
    {
        return $this->notes;
    }

    public function setNotes(?string $notes): static
    {
        $this->notes = $notes;

        return $this;
    }

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

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

        return $this;
    }

    public function getReferenceFormula(): ?string
    {
        return $this->referenceFormula;
    }

    public function setReferenceFormula(?string $referenceFormula): static
    {
        $this->referenceFormula = $referenceFormula;

        return $this;
    }

    public function getRequiredFieldsForReference(): ?array
    {
        return $this->requiredFieldsForReference;
    }

    public function setRequiredFieldsForReference(?array $requiredFieldsForReference): static
    {
        $this->requiredFieldsForReference = $requiredFieldsForReference;

        return $this;
    }

    #[Groups(['model_type:read', 'product:read', 'composant:read', 'piece:read'])]
    public function getStructure(): ?array
    {
        return match ($this->category) {
            ModelCategory::COMPONENT => $this->getComponentStructureFromRelations(),
            ModelCategory::PIECE     => $this->getPieceStructureFromRelations(),
            ModelCategory::PRODUCT   => ['customFields' => $this->serializeCustomFields($this->productCustomFields)],
        };
    }

    #[Groups(['model_type:write'])]
    public function setStructure(?array $structure): static
    {
        $this->pendingStructure = $structure;

        return $this;
    }

    public function getPendingStructure(): ?array
    {
        return $this->pendingStructure;
    }

    public function clearPendingStructure(): void
    {
        $this->pendingStructure = null;
    }

    /**
     * @return Collection<int, CustomField>
     */
    public function getComponentCustomFields(): Collection
    {
        return $this->customFields;
    }

    /**
     * @return Collection<int, CustomField>
     */
    public function getPieceCustomFields(): Collection
    {
        return $this->pieceCustomFields;
    }

    /**
     * @return Collection<int, CustomField>
     */
    public function getProductCustomFields(): Collection
    {
        return $this->productCustomFields;
    }

    /**
     * @return Collection<int, SkeletonPieceRequirement>
     */
    public function getSkeletonPieceRequirements(): Collection
    {
        return $this->skeletonPieceRequirements;
    }

    public function addSkeletonPieceRequirement(SkeletonPieceRequirement $requirement): static
    {
        if (!$this->skeletonPieceRequirements->contains($requirement)) {
            $this->skeletonPieceRequirements->add($requirement);
            $requirement->setModelType($this);
        }

        return $this;
    }

    public function removeSkeletonPieceRequirement(SkeletonPieceRequirement $requirement): static
    {
        $this->skeletonPieceRequirements->removeElement($requirement);

        return $this;
    }

    /**
     * @return Collection<int, SkeletonProductRequirement>
     */
    public function getSkeletonProductRequirements(): Collection
    {
        return $this->skeletonProductRequirements;
    }

    public function addSkeletonProductRequirement(SkeletonProductRequirement $requirement): static
    {
        if (!$this->skeletonProductRequirements->contains($requirement)) {
            $this->skeletonProductRequirements->add($requirement);
            $requirement->setModelType($this);
        }

        return $this;
    }

    public function removeSkeletonProductRequirement(SkeletonProductRequirement $requirement): static
    {
        $this->skeletonProductRequirements->removeElement($requirement);

        return $this;
    }

    /**
     * @return Collection<int, SkeletonSubcomponentRequirement>
     */
    public function getSkeletonSubcomponentRequirements(): Collection
    {
        return $this->skeletonSubcomponentRequirements;
    }

    public function addSkeletonSubcomponentRequirement(SkeletonSubcomponentRequirement $requirement): static
    {
        if (!$this->skeletonSubcomponentRequirements->contains($requirement)) {
            $this->skeletonSubcomponentRequirements->add($requirement);
            $requirement->setModelType($this);
        }

        return $this;
    }

    public function removeSkeletonSubcomponentRequirement(SkeletonSubcomponentRequirement $requirement): static
    {
        $this->skeletonSubcomponentRequirements->removeElement($requirement);

        return $this;
    }

    private function getComponentStructureFromRelations(): array
    {
        $structure = ['customFields' => $this->serializeCustomFields($this->customFields), 'pieces' => [], 'products' => [], 'subcomponents' => []];

        foreach ($this->skeletonPieceRequirements as $req) {
            $structure['pieces'][] = [
                'typePieceId' => $req->getTypePiece()->getId(),
            ];
        }

        foreach ($this->skeletonProductRequirements as $req) {
            $structure['products'][] = [
                'typeProductId' => $req->getTypeProduct()->getId(),
                'familyCode'    => $req->getFamilyCode(),
            ];
        }

        foreach ($this->skeletonSubcomponentRequirements as $req) {
            $structure['subcomponents'][] = [
                'alias'           => $req->getAlias(),
                'familyCode'      => $req->getFamilyCode(),
                'typeComposantId' => $req->getTypeComposant()?->getId(),
            ];
        }

        return $structure;
    }

    private function getPieceStructureFromRelations(): array
    {
        return [
            'customFields' => $this->serializeCustomFields($this->pieceCustomFields),
            'products'     => array_map(fn (SkeletonProductRequirement $req) => [
                'typeProductId' => $req->getTypeProduct()->getId(),
                'familyCode'    => $req->getFamilyCode(),
            ], $this->skeletonProductRequirements->toArray()),
        ];
    }

    /**
     * @param Collection<int, CustomField> $fields
     */
    private function serializeCustomFields(Collection $fields): array
    {
        $items = [];
        foreach ($fields as $cf) {
            $items[] = [
                'id'                 => $cf->getId(),
                'name'               => $cf->getName(),
                'type'               => $cf->getType(),
                'required'           => $cf->isRequired(),
                'options'            => $cf->getOptions(),
                'defaultValue'       => $cf->getDefaultValue(),
                'orderIndex'         => $cf->getOrderIndex(),
                'machineContextOnly' => $cf->isMachineContextOnly(),
            ];
        }

        usort($items, static fn (array $a, array $b) => $a['orderIndex'] <=> $b['orderIndex']);

        return $items;
    }
}
