<?php

declare(strict_types=1);

namespace App\Module\Core\Domain\Entity;

use ApiPlatform\Doctrine\Orm\Filter\BooleanFilter;
use ApiPlatform\Doctrine\Orm\Filter\SearchFilter;
use ApiPlatform\Metadata\ApiFilter;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use App\Module\Core\Infrastructure\Doctrine\DoctrinePermissionRepository;
use App\Shared\Domain\Attribute\Auditable;
use Doctrine\ORM\Mapping as ORM;
use InvalidArgumentException;
use Symfony\Component\Serializer\Attribute\Groups;

#[ApiResource(
    operations: [
        // Guard RBAC du catalogue de permissions : accepte les gestionnaires
        // de users et de roles en plus du code dedie `core.permissions.view`.
        // Justification : les drawers `UserRbacDrawer`/`RoleDrawer` fetchent
        // systematiquement ce catalogue pour afficher les checkboxes de
        // permissions ; exiger uniquement `core.permissions.view` casserait
        // ces workflows pour tout gestionnaire non-admin. L'endpoint n'expose
        // que des codes/libelles (pas de secret), le bypass reste acceptable.
        new GetCollection(
            normalizationContext: ['groups' => ['permission:read']],
            security: "is_granted('core.permissions.view') or is_granted('core.users.manage') or is_granted('core.roles.manage')",
        ),
        new Get(
            normalizationContext: ['groups' => ['permission:read']],
            security: "is_granted('core.permissions.view') or is_granted('core.users.manage') or is_granted('core.roles.manage')",
        ),
    ],
)]
#[ApiFilter(SearchFilter::class, properties: ['module' => 'exact'])]
#[ApiFilter(BooleanFilter::class, properties: ['orphan'])]
#[ORM\Entity(repositoryClass: DoctrinePermissionRepository::class)]
#[ORM\Table(name: 'permission')]
#[Auditable]
#[ORM\UniqueConstraint(name: 'uniq_permission_code', columns: ['code'])]
#[ORM\Index(name: 'idx_permission_module', columns: ['module'])]
#[ORM\Index(name: 'idx_permission_orphan', columns: ['orphan'])]
class Permission
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    #[Groups(['permission:read'])]
    private ?int $id = null;

    #[ORM\Column(length: 255)]
    #[Groups(['permission:read'])]
    private string $code;

    #[ORM\Column(length: 255)]
    #[Groups(['permission:read'])]
    private string $label;

    #[ORM\Column(length: 100)]
    #[Groups(['permission:read'])]
    private string $module;

    #[ORM\Column(options: ['default' => false])]
    #[Groups(['permission:read'])]
    private bool $orphan = false;

    /**
     * Invariants : une permission doit avoir un code non vide respectant la
     * convention "module.resource[.sub].action" (donc contenir au moins un
     * point), un libelle non vide et un module proprietaire non vide. Ces
     * garde-fous evitent la persistence de lignes incoherentes si un appelant
     * (fixture, commande de synchro, import) oublie un champ ou passe une
     * chaine vide.
     */
    public function __construct(string $code, string $label, string $module)
    {
        if ('' === $code) {
            throw new InvalidArgumentException('Le code de permission ne peut pas etre vide.');
        }
        if (!str_contains($code, '.')) {
            throw new InvalidArgumentException(sprintf('Le code de permission "%s" ne respecte pas la convention "module.resource[.sub].action".', $code));
        }
        if ('' === $label) {
            throw new InvalidArgumentException('Le libelle de permission ne peut pas etre vide.');
        }
        if ('' === $module) {
            throw new InvalidArgumentException('Le module proprietaire de la permission ne peut pas etre vide.');
        }

        $this->code   = $code;
        $this->label  = $label;
        $this->module = $module;
    }

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

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

    public function getLabel(): string
    {
        return $this->label;
    }

    public function getModule(): string
    {
        return $this->module;
    }

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

    /**
     * Marque la permission comme orpheline : son code n'est plus declare par
     * aucun module. Elle reste en base pour preserver les assignations et
     * permettre une reactivation ulterieure, mais doit etre ignoree par les
     * verifications d'autorisation.
     */
    public function markOrphan(): static
    {
        $this->orphan = true;

        return $this;
    }

    /**
     * Reactive une permission precedemment orpheline : son code reapparait
     * dans le code source d'un module. Equivaut a updateMetadata() suivi d'un
     * clearing du flag orphan ; on delegue a updateMetadata() pour ne pas
     * dupliquer la logique d'affectation des metadonnees.
     */
    public function revive(string $label, string $module): static
    {
        $this->updateMetadata($label, $module);
        $this->orphan = false;

        return $this;
    }

    /**
     * Met a jour les metadonnees d'une permission active sans toucher a son
     * statut d'orphelin. Utilise par la commande de synchronisation lorsque
     * seul le libelle ou le module proprietaire a change cote code.
     */
    public function updateMetadata(string $label, string $module): static
    {
        $this->label  = $label;
        $this->module = $module;

        return $this;
    }
}
