<?php

declare(strict_types=1);

namespace App\Module\Sites\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\Core\Domain\Entity\User;
use App\Module\Sites\Infrastructure\Doctrine\DoctrineSiteRepository;
use App\Shared\Domain\Attribute\Auditable;
use App\Shared\Domain\Contract\SiteInterface;
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\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Serializer\Attribute\Groups;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * Site physique (usine / etablissement) appartenant a l'instance Starseed.
 *
 * Adresse decomposee en champs structures (rue, complement, CP, ville) pour
 * permettre des recherches/tris fins ulterieurs et eviter les divergences
 * entre champs duplique. La methode `getFullAddress()` fournit la version
 * concatenee multi-lignes pour les usages d'affichage.
 *
 * Expose en API Platform pour l'administration CRUD avec RBAC :
 *  - lecture (GET list / item) : requiert la permission `sites.view`
 *  - ecriture (POST / PATCH / DELETE) : requiert la permission `sites.manage`
 *
 * Egalement embarque dans la reponse `/api/me` (groupe `me:read`) pour que
 * le frontend connaisse les sites autorises et le site courant de l'user.
 */
#[ApiResource(
    operations: [
        // Lecture (liste + item) : permission d'administration `sites.view` OU
        // permission de lecture-referentiel transverse `sites.read_ref` (ERP-102).
        // Le referentiel sites alimente les selects d'adresse des modules Tiers :
        // tout role qui gere des tiers doit pouvoir le lire sans porter l'acces
        // admin des Sites.
        new GetCollection(
            normalizationContext: ['groups' => ['site:read']],
            security: "is_granted('sites.view') or is_granted('sites.read_ref')",
        ),
        new Get(
            normalizationContext: ['groups' => ['site:read']],
            security: "is_granted('sites.view') or is_granted('sites.read_ref')",
        ),
        new Post(
            normalizationContext: ['groups' => ['site:read']],
            denormalizationContext: ['groups' => ['site:write']],
            security: "is_granted('sites.manage')",
        ),
        new Patch(
            normalizationContext: ['groups' => ['site:read']],
            denormalizationContext: ['groups' => ['site:write']],
            security: "is_granted('sites.manage')",
        ),
        new Delete(security: "is_granted('sites.manage')"),
    ],
    normalizationContext: ['groups' => ['site:read']],
    denormalizationContext: ['groups' => ['site:write']],
)]
#[ORM\Entity(repositoryClass: DoctrineSiteRepository::class)]
#[ORM\Table(name: 'site')]
#[Auditable]
#[ORM\UniqueConstraint(name: 'uniq_site_name', columns: ['name'])]
#[ORM\HasLifecycleCallbacks]
#[UniqueEntity(fields: ['name'], message: 'Un site avec ce nom existe deja.')]
class Site implements SiteInterface
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    #[Groups(['site:read', 'me:read'])]
    private ?int $id = null;

    #[ORM\Column(length: 100)]
    #[Assert\NotBlank(message: 'Le nom du site est requis.')]
    #[Assert\Length(max: 100, maxMessage: 'Le nom du site ne peut pas depasser {{ limit }} caracteres.')]
    #[Groups(['site:read', 'site:write', 'me:read'])]
    private string $name;

    // Premiere ligne d'adresse : numero + voie. Requise.
    #[ORM\Column(length: 255)]
    #[Assert\NotBlank(message: 'La rue est requise.')]
    #[Assert\Length(max: 255, maxMessage: 'La rue ne peut pas depasser {{ limit }} caracteres.')]
    #[Groups(['site:read', 'site:write', 'me:read'])]
    private string $street;

    // Complement d'adresse optionnel : batiment, escalier, BP, etc.
    #[ORM\Column(length: 255, nullable: true)]
    #[Assert\Length(max: 255, maxMessage: 'Le complement ne peut pas depasser {{ limit }} caracteres.')]
    #[Groups(['site:read', 'site:write', 'me:read'])]
    private ?string $complement = null;

    // Colonne mappee sur le snake_case PostgreSQL (convention projet : noms de
    // colonnes en minuscules dans le SQL brut). Le format est contraint au
    // code postal francais strict : 5 chiffres numeriques.
    #[ORM\Column(name: 'postal_code', length: 10)]
    #[Assert\NotBlank(message: 'Le code postal est requis.')]
    #[Assert\Length(max: 10, maxMessage: 'Le code postal ne peut pas depasser {{ limit }} caracteres.')]
    #[Assert\Regex(
        pattern: '/^\d{5}$/',
        message: 'Le code postal doit etre compose de 5 chiffres (format FR).',
    )]
    #[Groups(['site:read', 'site:write', 'me:read'])]
    private string $postalCode;

    #[ORM\Column(length: 100)]
    #[Assert\NotBlank(message: 'La ville du site est requise.')]
    #[Assert\Length(max: 100, maxMessage: 'La ville ne peut pas depasser {{ limit }} caracteres.')]
    #[Groups(['site:read', 'site:write', 'me:read'])]
    private string $city;

    // Couleur d'identification visuelle du site au format hex #RRGGBB (7 chars
    // incluant le diese). Utilisee par la navbar (ticket 3) pour distinguer
    // les sites d'un coup d'oeil.
    #[ORM\Column(length: 7)]
    #[Assert\NotBlank(message: 'La couleur est requise.')]
    #[Assert\Regex(
        pattern: '/^#[0-9A-Fa-f]{6}$/',
        message: 'La couleur doit etre un code hex de 7 caracteres au format #RRGGBB.',
    )]
    #[Groups(['site:read', 'site:write', 'me:read'])]
    private string $color;

    // createdAt / updatedAt volontairement exclus du groupe `me:read` :
    // le payload `/api/me` doit rester leger, ces metadonnees ne sont utiles
    // qu'a l'admin (exposees uniquement via `site:read` sur /api/sites).
    #[ORM\Column(name: 'created_at', type: Types::DATETIME_IMMUTABLE)]
    #[Groups(['site:read'])]
    private DateTimeImmutable $createdAt;

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

    /**
     * Collection inverse des users rattaches a ce site.
     *
     * Volontairement SANS `#[Groups]` : la collection n'est jamais exposee via
     * l'API pour deux raisons :
     *  - eviter une boucle de serialisation infinie User → sites → users → ...
     *    si un jour un developpeur ajoute `me:read` ici par megarde ;
     *  - l'inverse n'a de valeur qu'en interne (compter les users d'un site,
     *    iterer en test de cascade).
     *
     * @var Collection<int, User>
     */
    #[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'sites')]
    private Collection $users;

    public function __construct(
        string $name,
        string $street,
        ?string $complement,
        string $postalCode,
        string $city,
        string $color,
    ) {
        $this->name       = $name;
        $this->street     = $street;
        $this->complement = $complement;
        $this->postalCode = $postalCode;
        $this->city       = $city;
        $this->color      = $color;
        $now              = new DateTimeImmutable();
        $this->createdAt  = $now;
        $this->updatedAt  = $now;
        $this->users      = new ArrayCollection();
    }

    /**
     * Callback Doctrine : a chaque update en base on rafraichit updatedAt.
     * Ne pas toucher a createdAt ici (immutable apres creation).
     */
    #[ORM\PreUpdate]
    public function onPreUpdate(): void
    {
        $this->updatedAt = new DateTimeImmutable();
    }

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

    public function getStreet(): string
    {
        return $this->street;
    }

    public function setStreet(string $street): static
    {
        $this->street = $street;

        return $this;
    }

    public function getComplement(): ?string
    {
        return $this->complement;
    }

    public function setComplement(?string $complement): static
    {
        $this->complement = $complement;

        return $this;
    }

    public function getPostalCode(): string
    {
        return $this->postalCode;
    }

    public function setPostalCode(string $postalCode): static
    {
        $this->postalCode = $postalCode;

        return $this;
    }

    public function getCity(): string
    {
        return $this->city;
    }

    public function setCity(string $city): static
    {
        $this->city = $city;

        return $this;
    }

    public function getColor(): string
    {
        return $this->color;
    }

    public function setColor(string $color): static
    {
        $this->color = $color;

        return $this;
    }

    /**
     * Adresse complete reconstituee : street, [complement,] {CP} {ville},
     * separes par des sauts de ligne. Methode pure, jamais persistee.
     *
     * Expose en lecture API (groupes site:read + me:read) pour que les
     * consommateurs (frontend, exports PDF) recoivent une adresse prete a
     * afficher sans dupliquer la logique de concatenation cote client.
     */
    #[Groups(['site:read', 'me:read'])]
    public function getFullAddress(): string
    {
        $lines = [$this->street];

        if (null !== $this->complement && '' !== trim($this->complement)) {
            $lines[] = $this->complement;
        }

        $lines[] = sprintf('%s %s', $this->postalCode, $this->city);

        return implode("\n", $lines);
    }

    public function getCreatedAt(): DateTimeImmutable
    {
        return $this->createdAt;
    }

    public function getUpdatedAt(): DateTimeImmutable
    {
        return $this->updatedAt;
    }

    /**
     * @return Collection<int, User>
     */
    public function getUsers(): Collection
    {
        return $this->users;
    }

    /**
     * Synchronise la collection inverse cote Site quand User::addSite est
     * appele. Idempotent. Ne re-appelle pas $user->addSite($this) pour
     * eviter une recursion infinie : User::addSite est le point d'entree
     * unique de la mutation.
     *
     * @internal Appele uniquement par User::addSite()
     */
    public function addUser(User $user): static
    {
        if (!$this->users->contains($user)) {
            $this->users->add($user);
        }

        return $this;
    }

    /**
     * @internal Appele uniquement par User::removeSite()
     */
    public function removeUser(User $user): static
    {
        $this->users->removeElement($user);

        return $this;
    }
}
