<?php

declare(strict_types=1);

namespace App\Module\Core\Infrastructure\Security;

use App\Module\Core\Domain\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

/**
 * Voter RBAC qui evalue les codes de permission metier au format
 * "module.resource.action" (ex: "core.users.view").
 *
 * - Ignore silencieusement les attributs non-RBAC (ROLE_*, IS_AUTHENTICATED_*, ...),
 *   qui restent traites par les voters core de Symfony. Strategy 'affirmative'
 *   par defaut : tant qu'un voter repond GRANTED, l'acces est accorde.
 * - Bypass total si l'utilisateur porte le flag isAdmin (decision architecturale
 *   gravee au ticket #343 section 11 : is_admin est le seul levier technique
 *   de bypass, jamais remplace par un check de role).
 * - Sinon, compare l'attribut aux permissions effectives de l'utilisateur
 *   (union dedupliquee triee venant des roles et des permissions directes).
 *
 * @extends Voter<string, mixed>
 */
final class PermissionVoter extends Voter
{
    /**
     * Regex de reconnaissance des codes de permission.
     *
     * Contraintes :
     *  - Premier caractere alphabetique minuscule (pas de chiffre, pas de ROLE_).
     *  - Au moins un point de separation (ecarte les attributs atomiques
     *    type ROLE_ADMIN ou IS_AUTHENTICATED_FULLY).
     *  - Segments en snake_case minuscule coherents avec les permissions
     *    declarees par les *Module::permissions() et validees par app:sync-permissions.
     */
    private const string PERMISSION_CODE_PATTERN = '/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/';

    protected function supports(string $attribute, mixed $subject): bool
    {
        return (bool) preg_match(self::PERMISSION_CODE_PATTERN, $attribute);
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
    {
        $user = $token->getUser();

        if (!$user instanceof User) {
            // Token anonyme ou user d'un autre type : on refuse explicitement.
            // Les voters core (AuthenticatedVoter) se chargent deja du cas
            // "pas authentifie du tout".
            return false;
        }

        if ($user->isAdmin()) {
            // Bypass total : decision architecturale #343 section 11.
            // Cette regle est dupliquee cote front dans usePermissions()
            // et les deux doivent bouger ensemble si elle evolue un jour.
            return true;
        }

        return in_array($attribute, $user->getEffectivePermissions(), true);
    }
}
