<?php

declare(strict_types=1);

namespace App\Module\Catalog\Domain\Entity;

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 App\Module\Catalog\Infrastructure\ApiPlatform\State\Processor\CategoryProcessor;
use App\Module\Catalog\Infrastructure\ApiPlatform\State\Provider\CategoryProvider;
use App\Module\Catalog\Infrastructure\Doctrine\DoctrineCategoryRepository;
use App\Shared\Domain\Attribute\Auditable;
use App\Shared\Domain\Contract\BlamableInterface;
use App\Shared\Domain\Contract\CategoryInterface;
use App\Shared\Domain\Contract\TimestampableInterface;
use App\Shared\Domain\Trait\TimestampableBlamableTrait;
use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * Categorie : referentiel metier classifiant les futurs tiers (clients,
 * fournisseurs, prestataires). Porte un `name` libre et un ou plusieurs
 * `categoryTypes` (ManyToMany vers le referentiel statique CategoryType,
 * table de jonction `category_category_type`). Une categorie peut appartenir
 * a plusieurs types simultanement (>= 1 obligatoire, RG-1.05).
 *
 * - Soft delete via `deletedAt` (pas de hard delete) : la liste exclut par
 *   defaut les categories supprimees (cf. CategoryProvider, ticket 0.3).
 * - Timestampable + Blamable via le Trait Shared : les 4 colonnes
 *   created_at / updated_at / created_by / updated_by sont remplies
 *   automatiquement par le TimestampableBlamableSubscriber.
 * - `#[Auditable]` : chaque create / update / delete (soft) est trace dans
 *   audit_log par l'AuditListener du module Core.
 *
 * Provider (filtre soft-delete + ?includeDeleted + tri name ASC + 404 sur
 * soft-deleted) et Processor (trim, 409 sur doublon, soft delete) branches
 * au ticket 0.3 (ERP-45).
 */
#[ApiResource(
    operations: [
        // Lecture (liste + item) : permission d'administration `view` OU permission
        // de lecture-referentiel transverse `read_ref` (ERP-102). Les referentiels
        // categories sont consommes par les modules Tiers (selects creation/filtre
        // client) : tout role qui gere des tiers doit pouvoir les lire sans porter
        // l'acces admin du Catalogue. `read_ref` est une permission Catalog (pas un
        // code d'un autre module) -> isolement inter-module preserve.
        new GetCollection(
            security: "is_granted('catalog.categories.view') or is_granted('catalog.categories.read_ref')",
            normalizationContext: ['groups' => ['category:read', 'default:read']],
            provider: CategoryProvider::class,
        ),
        new Get(
            security: "is_granted('catalog.categories.view') or is_granted('catalog.categories.read_ref')",
            normalizationContext: ['groups' => ['category:read', 'default:read']],
            provider: CategoryProvider::class,
        ),
        new Post(
            security: "is_granted('catalog.categories.manage')",
            normalizationContext: ['groups' => ['category:read', 'default:read']],
            denormalizationContext: ['groups' => ['category:write']],
            processor: CategoryProcessor::class,
        ),
        new Patch(
            security: "is_granted('catalog.categories.manage')",
            normalizationContext: ['groups' => ['category:read', 'default:read']],
            denormalizationContext: ['groups' => ['category:write']],
            provider: CategoryProvider::class,
            processor: CategoryProcessor::class,
        ),
        new Delete(
            security: "is_granted('catalog.categories.manage')",
            provider: CategoryProvider::class,
            processor: CategoryProcessor::class,
        ),
    ],
)]
#[ORM\Entity(repositoryClass: DoctrineCategoryRepository::class)]
#[ORM\Table(name: 'category')]
// Index nommes pour matcher la migration (cf. Role/Permission/Site). Les index
// uniques partiels `uq_category_name_active` (LOWER(name) WHERE deleted_at IS
// NULL — unicite GLOBALE du nom parmi les actifs) et `uq_category_code` (code
// WHERE deleted_at IS NULL) restent possedes par la seule migration : Doctrine
// ORM ne sait pas exprimer un index partiel via attribut.
#[ORM\Index(name: 'idx_category_deleted_at', columns: ['deleted_at'])]
#[ORM\Index(name: 'idx_category_created_by', columns: ['created_by'])]
#[ORM\Index(name: 'idx_category_updated_by', columns: ['updated_by'])]
#[Auditable]
class Category implements TimestampableInterface, BlamableInterface, CategoryInterface
{
    // === Timestampable + Blamable ===
    // Les 4 colonnes (created_at, updated_at, created_by, updated_by) + leurs
    // getters/setters viennent du Trait Shared. Le TimestampableBlamableSubscriber
    // les remplit automatiquement au prePersist / preUpdate. Aucune redeclaration
    // manuelle de ces proprietes ici.
    use TimestampableBlamableTrait;

    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    #[Groups(['category:read'])]
    private ?int $id = null;

    // RG-1.02 + RG-1.03 : un name compose uniquement d'espaces doit declencher
    // NotBlank. Le normalizer 'trim' fait le menage avant validation, alignant
    // le comportement sur le trim cote Processor (qui s'applique apres) : ainsi
    // POST {name: "   "} -> 422 et POST {name: "  Vis  "} -> 201 avec "Vis"
    // persiste, sans contradiction entre l'ordre Validate / Process.
    #[ORM\Column(length: 120)]
    #[Assert\NotBlank(message: 'Le nom est obligatoire.', normalizer: 'trim')]
    #[Assert\Length(min: 2, max: 120, minMessage: 'Le nom doit comporter au moins {{ limit }} caractères.', maxMessage: 'Le nom ne peut dépasser {{ limit }} caractères.', normalizer: 'trim')]
    #[Groups(['category:read', 'category:write'])]
    private ?string $name = null;

    // Code technique stable (slug MAJUSCULE du nom) — NOT NULL + unique parmi les
    // actifs (index partiel `uq_category_code` possede par la migration). Genere
    // par le CategoryProcessor a la creation puis fige (jamais recalcule sur
    // renommage) : sert de cle metier deterministe (RG-1.03 / RG-1.29). Lecture
    // seule cote API (hors groupe category:write) : le front filtre dessus mais
    // ne le saisit pas.
    #[ORM\Column(length: 50)]
    #[Groups(['category:read'])]
    private ?string $code = null;

    /**
     * Types de la categorie (>= 1 obligatoire, RG-1.05). ManyToMany vers le
     * referentiel statique CategoryType via la jonction `category_category_type`.
     * Cote inverse (category_type) en ON DELETE RESTRICT : un type ne peut etre
     * supprime tant qu'il reste reference par une categorie.
     *
     * @var Collection<int, CategoryType>
     */
    #[ORM\ManyToMany(targetEntity: CategoryType::class)]
    #[ORM\JoinTable(name: 'category_category_type')]
    #[ORM\JoinColumn(name: 'category_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
    #[ORM\InverseJoinColumn(name: 'category_type_id', referencedColumnName: 'id', onDelete: 'RESTRICT')]
    #[Assert\Count(min: 1, minMessage: 'Sélectionnez au moins un type de catégorie.')]
    #[Groups(['category:read', 'category:write'])]
    private Collection $categoryTypes;

    /**
     * Soft delete : null = active, valeur = supprimee logiquement le {date}.
     * Pas exposee en ecriture (groupe category:write exclu) : seul le DELETE,
     * via le CategoryProcessor (ticket 0.3), pose la valeur.
     */
    #[ORM\Column(name: 'deleted_at', type: 'datetime_immutable', nullable: true)]
    #[Groups(['category:read'])]
    private ?DateTimeImmutable $deletedAt = null;

    public function __construct()
    {
        $this->categoryTypes = new ArrayCollection();
    }

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

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

    public function setName(string $name): static
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Implemente CategoryInterface : code technique stable de la categorie.
     */
    public function getCode(): ?string
    {
        return $this->code;
    }

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

        return $this;
    }

    /**
     * @return Collection<int, CategoryType>
     */
    public function getCategoryTypes(): Collection
    {
        return $this->categoryTypes;
    }

    public function addCategoryType(CategoryType $categoryType): static
    {
        if (!$this->categoryTypes->contains($categoryType)) {
            $this->categoryTypes->add($categoryType);
        }

        return $this;
    }

    public function removeCategoryType(CategoryType $categoryType): static
    {
        $this->categoryTypes->removeElement($categoryType);

        return $this;
    }

    /**
     * Implemente CategoryInterface : liste des codes de types rattaches a la
     * categorie. Permet aux modules tiers de filtrer/valider par type metier
     * (ex: RG-2.10 « contient FOURNISSEUR ») sans dependre de Catalog.
     *
     * @return list<string>
     */
    public function getCategoryTypeCodes(): array
    {
        return array_values(array_filter(
            $this->categoryTypes->map(static fn (CategoryType $t): ?string => $t->getCode())->toArray(),
        ));
    }

    public function getDeletedAt(): ?DateTimeImmutable
    {
        return $this->deletedAt;
    }

    public function setDeletedAt(?DateTimeImmutable $deletedAt): static
    {
        $this->deletedAt = $deletedAt;

        return $this;
    }
}
