<?php

namespace App\Security\Voter;

use App\Entity\ScanSession;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;

/**
 * Permissions sur les scans, combinant le groupe de l'utilisateur et le périmètre par concession.
 *
 * - VIEW           : ADMIN/AUDIT partout ; SUPERVISEUR/OPERATEUR sur leurs concessions.
 * - ARCHIVE        : tant que le scan n'est pas « Validé » → ADMIN/AUDIT partout, SUPERVISEUR/
 *                    OPERATEUR sur leurs concessions ; une fois « Validé » → ADMIN uniquement.
 * - CHANGE_STATUS  : ADMIN/AUDIT partout ; SUPERVISEUR sur ses concessions ; OPERATEUR jamais.
 * - SUBMIT         : soumettre à la relecture (en_cours → a_valider) — même portée que VIEW
 *                    (OPERATEUR inclus), uniquement tant que le statut est « En cours ».
 * - EDIT_DOCUMENTS : même portée que VIEW, mais uniquement tant que le statut est « En cours ».
 * - EDIT_CONCESSIONS : modifier les concessions associées, uniquement AVANT validation
 *                    (statut ≠ « Validé ») — OPERATEUR en périmètre tant que « En cours »,
 *                    SUPERVISEUR/AUDIT/ADMIN aussi sur « À valider » / « Annulé ».
 * - DELETE         : tant que le scan n'est pas « Validé » → ADMIN/AUDIT partout, SUPERVISEUR/
 *                    OPERATEUR sur leurs concessions ; une fois « Validé » → ADMIN uniquement.
 * - CREATE (sujet null) : tout groupe actif, SAUF LECTEUR.
 * - LECTEUR : lecture seule — uniquement VIEW (dans son périmètre), aucune action.
 *
 * @extends Voter<string, ScanSession|null>
 */
final class ScanVoter extends Voter
{
    public const VIEW = 'SCAN_VIEW';
    public const CREATE = 'SCAN_CREATE';
    public const ARCHIVE = 'SCAN_ARCHIVE';
    public const CHANGE_STATUS = 'SCAN_CHANGE_STATUS';
    public const SUBMIT = 'SCAN_SUBMIT';
    public const EDIT_DOCUMENTS = 'SCAN_EDIT_DOCUMENTS';
    public const EDIT_CONCESSIONS = 'SCAN_EDIT_CONCESSIONS';
    public const DELETE = 'SCAN_DELETE';

    private const ATTRIBUTES = [
        self::VIEW,
        self::CREATE,
        self::ARCHIVE,
        self::CHANGE_STATUS,
        self::SUBMIT,
        self::EDIT_DOCUMENTS,
        self::EDIT_CONCESSIONS,
        self::DELETE,
    ];

    public function __construct(
        private readonly RoleHierarchyInterface $roleHierarchy,
    ) {
    }

    protected function supports(string $attribute, mixed $subject): bool
    {
        if (!\in_array($attribute, self::ATTRIBUTES, true)) {
            return false;
        }

        return self::CREATE === $attribute || $subject instanceof ScanSession;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();
        if (!$user instanceof User || !$user->isActive()) {
            return false;
        }

        $reachable = $this->roleHierarchy->getReachableRoleNames($user->getRoles());
        $isAdmin = \in_array(User::ROLE_ADMIN, $reachable, true);
        $seesAll = $user->seesAllConcessions();

        // LECTEUR : lecture seule. Il peut consulter (VIEW) dans son périmètre, mais aucune
        // action (création, archivage, changement de statut, édition de documents, suppression).
        if (User::ROLE_LECTEUR === $user->getGroup() && self::VIEW !== $attribute) {
            return false;
        }

        return match ($attribute) {
            self::CREATE => null !== $user->getGroup(),
            // Suppression : libre (selon périmètre) tant que le scan n'est pas validé ;
            // une fois validé, ADMIN uniquement.
            self::DELETE => $isAdmin
                || (ScanSession::STATUS_VALIDE !== $subject?->getStatus()
                    && ($seesAll || $this->inScope($user, $subject))),
            self::VIEW => $seesAll || $this->inScope($user, $subject),
            // Archivage : même portée que VIEW tant que non validé ; ADMIN seul une fois validé.
            self::ARCHIVE => ScanSession::STATUS_VALIDE === $subject?->getStatus()
                ? $isAdmin
                : ($seesAll || $this->inScope($user, $subject)),
            self::CHANGE_STATUS => User::ROLE_OPERATEUR !== $user->getGroup()
                && ($seesAll || $this->inScope($user, $subject)),
            // Soumettre à la relecture (en_cours → a_valider) : ouvert à l'OPERATEUR en périmètre.
            self::SUBMIT => $subject instanceof ScanSession
                && ScanSession::STATUS_EN_COURS === $subject->getStatus()
                && ($seesAll || $this->inScope($user, $subject)),
            // Ajout/suppression de pièces jointes : seulement tant que l'inventaire est « En cours ».
            self::EDIT_DOCUMENTS => $subject instanceof ScanSession
                && ScanSession::STATUS_EN_COURS === $subject->getStatus()
                && ($seesAll || $this->inScope($user, $subject)),
            // Concessions modifiables tant que le scan n'est pas validé (le rouvrir sinon).
            // L'OPERATEUR ne peut corriger que tant que le scan est « En cours ».
            self::EDIT_CONCESSIONS => $subject instanceof ScanSession
                && ScanSession::STATUS_VALIDE !== $subject->getStatus()
                && ($seesAll || $this->inScope($user, $subject))
                && (User::ROLE_OPERATEUR !== $user->getGroup()
                    || ScanSession::STATUS_EN_COURS === $subject->getStatus()),
            default => false,
        };
    }

    private function inScope(User $user, mixed $subject): bool
    {
        if (!$subject instanceof ScanSession) {
            return false;
        }

        return [] !== array_intersect($subject->getConcessions(), $user->getConcessions());
    }
}
