<?php

declare(strict_types=1);

namespace App\Module\Core\Domain\Security;

use App\Module\Core\Domain\Entity\User;
use App\Module\Core\Domain\Exception\LastAdminProtectionException;
use App\Module\Core\Domain\Repository\UserRepositoryInterface;

/**
 * Gardien de l'invariant domaine : l'instance doit toujours conserver
 * au moins un utilisateur administrateur.
 *
 * Ce service est appele avant toute operation susceptible de reduire le
 * nombre d'admins (retrait du flag isAdmin, suppression d'un utilisateur).
 * Il compte les admins restants et leve LastAdminProtectionException si
 * le seuil minimum (1) serait franchi.
 */
final class AdminHeadcountGuard implements AdminHeadcountGuardInterface
{
    public function __construct(private readonly UserRepositoryInterface $userRepository) {}

    /**
     * Verifie qu'il restera au moins un admin apres la demote de $user.
     *
     * L'argument $user est accepte mais non utilise dans la logique de comptage :
     * l'appelant a deja determine que cet utilisateur va perdre son statut admin ;
     * le garde se contente de verifier qu'il en reste au moins un autre.
     * Le parametre est conserve pour la lisibilite du site d'appel et pour
     * permettre une evolution future (ex : journalisation, audit trail).
     */
    public function ensureAtLeastOneAdminRemainsAfterDemotion(User $user): void
    {
        $this->checkAdminHeadcount();
    }

    /**
     * Verifie qu'il restera au moins un admin apres la suppression de $user.
     *
     * Meme principe que ensureAtLeastOneAdminRemainsAfterDemotion() : $user
     * est accepte pour la symetrie du contrat et les evolutions futures,
     * mais le comptage ne depend pas de son identite.
     */
    public function ensureAtLeastOneAdminRemainsAfterDeletion(User $user): void
    {
        $this->checkAdminHeadcount();
    }

    /**
     * Compte les administrateurs et leve une exception si le seuil minimum est atteint.
     *
     * La verification est volontairement conservative (<=1) pour couvrir
     * le cas defensif ou la base serait deja dans un etat incoherent (0 admin).
     *
     * TOCTOU accepte : la verification n'utilise pas de verrou pessimiste
     * (SELECT ... FOR UPDATE). Deux demotions concurrentes pourraient donc
     * passer le garde simultanement. Ce risque est accepte dans le contexte
     * PME/CRM ou les operations d'administration sont rares et mono-operateur.
     * Si la concurrence admin devient un enjeu, ajouter un verrou pessimiste
     * sur countAdmins() ou une contrainte CHECK en base.
     *
     * @throws LastAdminProtectionException si le nombre d'admins est inferieur ou egal a 1
     */
    private function checkAdminHeadcount(): void
    {
        if ($this->userRepository->countAdmins() <= 1) {
            throw new LastAdminProtectionException();
        }
    }
}
