"""
Système de contrôle d'accès basé sur les rôles (RBAC).

Ce module gère l'authentification et l'autorisation des utilisateurs
selon leur rôle (TIM, responsable DIM, administrateur).
"""

import hashlib
import logging
import secrets
from datetime import datetime, timedelta
from enum import Enum
from typing import Dict, List, Optional, Set

from pydantic import BaseModel, Field

logger = logging.getLogger(__name__)


class Role(str, Enum):
    """Rôles utilisateurs dans le système."""
    
    TIM = "tim"  # Technicien d'Information Médicale
    RESPONSABLE_DIM = "responsable_dim"  # Responsable DIM
    ADMINISTRATEUR = "administrateur"  # Administrateur système


class Permission(str, Enum):
    """Permissions disponibles dans le système."""
    
    # Permissions de lecture
    VIEW_CODES = "view_codes"  # Voir les codes proposés
    VIEW_AUDIT = "view_audit"  # Voir les audits
    VIEW_METRICS = "view_metrics"  # Voir les métriques
    
    # Permissions d'écriture
    CORRECT_CODE = "correct_code"  # Corriger un code
    VALIDATE_STAY = "validate_stay"  # Valider un séjour
    ADD_COMMENT = "add_comment"  # Ajouter un commentaire
    
    # Permissions d'export
    EXPORT_AUDIT = "export_audit"  # Exporter les audits
    EXPORT_METRICS = "export_metrics"  # Exporter les métriques
    
    # Permissions d'administration
    MANAGE_USERS = "manage_users"  # Gérer les utilisateurs
    MANAGE_REFERENTIELS = "manage_referentiels"  # Gérer les référentiels
    MANAGE_RULES = "manage_rules"  # Gérer les règles
    VIEW_SYSTEM_LOGS = "view_system_logs"  # Voir les logs système


# Mapping des rôles vers leurs permissions
ROLE_PERMISSIONS: Dict[Role, Set[Permission]] = {
    Role.TIM: {
        Permission.VIEW_CODES,
        Permission.VIEW_AUDIT,
        Permission.CORRECT_CODE,
        Permission.VALIDATE_STAY,
        Permission.ADD_COMMENT,
    },
    Role.RESPONSABLE_DIM: {
        Permission.VIEW_CODES,
        Permission.VIEW_AUDIT,
        Permission.VIEW_METRICS,
        Permission.CORRECT_CODE,
        Permission.VALIDATE_STAY,
        Permission.ADD_COMMENT,
        Permission.EXPORT_AUDIT,
        Permission.EXPORT_METRICS,
        Permission.MANAGE_RULES,
    },
    Role.ADMINISTRATEUR: set(Permission),  # Toutes les permissions
}


class User(BaseModel):
    """Utilisateur du système."""
    
    user_id: str = Field(..., description="Identifiant unique de l'utilisateur")
    username: str = Field(..., min_length=3, description="Nom d'utilisateur")
    password_hash: str = Field(..., description="Hash du mot de passe")
    role: Role = Field(..., description="Rôle de l'utilisateur")
    email: Optional[str] = Field(None, description="Email de l'utilisateur")
    full_name: Optional[str] = Field(None, description="Nom complet")
    is_active: bool = Field(default=True, description="Compte actif")
    created_at: datetime = Field(default_factory=datetime.now)
    last_login: Optional[datetime] = Field(None, description="Dernière connexion")
    
    class Config:
        frozen = True  # Immutable


class Session(BaseModel):
    """Session utilisateur."""
    
    session_id: str = Field(..., description="Identifiant de session")
    user_id: str = Field(..., description="Identifiant utilisateur")
    created_at: datetime = Field(default_factory=datetime.now)
    expires_at: datetime = Field(..., description="Date d'expiration")
    ip_address: Optional[str] = Field(None, description="Adresse IP")
    
    def is_expired(self) -> bool:
        """Vérifie si la session est expirée."""
        return datetime.now() > self.expires_at


class AccessControl:
    """
    Système de contrôle d'accès basé sur les rôles (RBAC).
    
    Ce système gère:
    - L'authentification des utilisateurs
    - L'autorisation basée sur les rôles
    - La gestion des sessions
    - Le hachage sécurisé des mots de passe
    
    Exigences: 17.1, 17.2, 17.3, 17.6
    """
    
    def __init__(self, session_duration_hours: int = 8):
        """
        Initialise le système de contrôle d'accès.
        
        Args:
            session_duration_hours: Durée de validité des sessions (défaut: 8h)
        """
        self.session_duration_hours = session_duration_hours
        self._users: Dict[str, User] = {}
        self._sessions: Dict[str, Session] = {}
        
        logger.info(
            f"AccessControl initialisé avec session_duration={session_duration_hours}h"
        )
    
    def create_user(
        self,
        username: str,
        password: str,
        role: Role,
        email: Optional[str] = None,
        full_name: Optional[str] = None
    ) -> User:
        """
        Crée un nouvel utilisateur.
        
        Args:
            username: Nom d'utilisateur (unique)
            password: Mot de passe en clair
            role: Rôle de l'utilisateur
            email: Email (optionnel)
            full_name: Nom complet (optionnel)
        
        Returns:
            Utilisateur créé
        
        Raises:
            ValueError: Si l'utilisateur existe déjà
        
        Exigences: 17.1, 17.6
        """
        # Vérifier que l'utilisateur n'existe pas déjà
        if any(u.username == username for u in self._users.values()):
            raise ValueError(f"L'utilisateur '{username}' existe déjà")
        
        # Générer un ID unique
        user_id = secrets.token_urlsafe(16)
        
        # Hasher le mot de passe
        password_hash = self._hash_password(password)
        
        # Créer l'utilisateur
        user = User(
            user_id=user_id,
            username=username,
            password_hash=password_hash,
            role=role,
            email=email,
            full_name=full_name
        )
        
        self._users[user_id] = user
        
        logger.info(f"Utilisateur créé: {username} (rôle: {role.value})")
        
        return user
    
    def authenticate(
        self,
        username: str,
        password: str,
        ip_address: Optional[str] = None
    ) -> Optional[Session]:
        """
        Authentifie un utilisateur et crée une session.
        
        Args:
            username: Nom d'utilisateur
            password: Mot de passe en clair
            ip_address: Adresse IP (optionnel)
        
        Returns:
            Session si authentification réussie, None sinon
        
        Exigences: 17.6
        """
        # Trouver l'utilisateur
        user = None
        for u in self._users.values():
            if u.username == username:
                user = u
                break
        
        if user is None:
            logger.warning(f"Tentative de connexion échouée: utilisateur '{username}' introuvable")
            return None
        
        # Vérifier que le compte est actif
        if not user.is_active:
            logger.warning(f"Tentative de connexion échouée: compte '{username}' inactif")
            return None
        
        # Vérifier le mot de passe
        if not self._verify_password(password, user.password_hash):
            logger.warning(f"Tentative de connexion échouée: mot de passe incorrect pour '{username}'")
            return None
        
        # Créer une session
        session_id = secrets.token_urlsafe(32)
        expires_at = datetime.now() + timedelta(hours=self.session_duration_hours)
        
        session = Session(
            session_id=session_id,
            user_id=user.user_id,
            expires_at=expires_at,
            ip_address=ip_address
        )
        
        self._sessions[session_id] = session
        
        # Mettre à jour la dernière connexion
        updated_user = User(
            user_id=user.user_id,
            username=user.username,
            password_hash=user.password_hash,
            role=user.role,
            email=user.email,
            full_name=user.full_name,
            is_active=user.is_active,
            created_at=user.created_at,
            last_login=datetime.now()
        )
        self._users[user.user_id] = updated_user
        
        logger.info(f"Authentification réussie: {username}")
        
        return session
    
    def get_user_from_session(self, session_id: str) -> Optional[User]:
        """
        Récupère l'utilisateur associé à une session.
        
        Args:
            session_id: Identifiant de session
        
        Returns:
            Utilisateur si session valide, None sinon
        """
        session = self._sessions.get(session_id)
        
        if session is None:
            return None
        
        if session.is_expired():
            logger.info(f"Session expirée: {session_id}")
            del self._sessions[session_id]
            return None
        
        return self._users.get(session.user_id)
    
    def has_permission(
        self,
        session_id: str,
        permission: Permission
    ) -> bool:
        """
        Vérifie si un utilisateur a une permission.
        
        Args:
            session_id: Identifiant de session
            permission: Permission à vérifier
        
        Returns:
            True si l'utilisateur a la permission, False sinon
        
        Exigences: 17.1, 17.2, 17.3
        """
        user = self.get_user_from_session(session_id)
        
        if user is None:
            return False
        
        # Récupérer les permissions du rôle
        role_permissions = ROLE_PERMISSIONS.get(user.role, set())
        
        has_perm = permission in role_permissions
        
        if not has_perm:
            logger.warning(
                f"Permission refusée: {user.username} (rôle: {user.role.value}) "
                f"n'a pas la permission '{permission.value}'"
            )
        
        return has_perm
    
    def require_permission(
        self,
        session_id: str,
        permission: Permission
    ) -> User:
        """
        Vérifie qu'un utilisateur a une permission (lève une exception sinon).
        
        Args:
            session_id: Identifiant de session
            permission: Permission requise
        
        Returns:
            Utilisateur si permission accordée
        
        Raises:
            PermissionError: Si l'utilisateur n'a pas la permission
        
        Exigences: 17.1, 17.2, 17.3
        """
        user = self.get_user_from_session(session_id)
        
        if user is None:
            raise PermissionError("Session invalide ou expirée")
        
        if not self.has_permission(session_id, permission):
            raise PermissionError(
                f"Permission refusée: '{permission.value}' requise "
                f"(rôle actuel: {user.role.value})"
            )
        
        return user
    
    def logout(self, session_id: str) -> bool:
        """
        Déconnecte un utilisateur (supprime la session).
        
        Args:
            session_id: Identifiant de session
        
        Returns:
            True si déconnexion réussie, False sinon
        """
        if session_id in self._sessions:
            user = self.get_user_from_session(session_id)
            if user:
                logger.info(f"Déconnexion: {user.username}")
            del self._sessions[session_id]
            return True
        
        return False
    
    def deactivate_user(self, user_id: str) -> bool:
        """
        Désactive un utilisateur.
        
        Args:
            user_id: Identifiant de l'utilisateur
        
        Returns:
            True si désactivation réussie, False sinon
        """
        user = self._users.get(user_id)
        
        if user is None:
            return False
        
        # Créer une nouvelle instance avec is_active=False
        updated_user = User(
            user_id=user.user_id,
            username=user.username,
            password_hash=user.password_hash,
            role=user.role,
            email=user.email,
            full_name=user.full_name,
            is_active=False,
            created_at=user.created_at,
            last_login=user.last_login
        )
        
        self._users[user_id] = updated_user
        
        # Supprimer toutes les sessions de cet utilisateur
        sessions_to_remove = [
            sid for sid, session in self._sessions.items()
            if session.user_id == user_id
        ]
        for sid in sessions_to_remove:
            del self._sessions[sid]
        
        logger.info(f"Utilisateur désactivé: {user.username}")
        
        return True
    
    def get_user_permissions(self, session_id: str) -> Set[Permission]:
        """
        Récupère toutes les permissions d'un utilisateur.
        
        Args:
            session_id: Identifiant de session
        
        Returns:
            Ensemble des permissions
        """
        user = self.get_user_from_session(session_id)
        
        if user is None:
            return set()
        
        return ROLE_PERMISSIONS.get(user.role, set())
    
    def _hash_password(self, password: str) -> str:
        """
        Hash un mot de passe avec SHA-256 et un salt.
        
        Note: Pour une utilisation en production, utiliser bcrypt ou argon2.
        
        Args:
            password: Mot de passe en clair
        
        Returns:
            Hash du mot de passe
        """
        # Générer un salt aléatoire
        salt = secrets.token_hex(16)
        
        # Hasher le mot de passe avec le salt
        password_with_salt = f"{salt}{password}"
        password_hash = hashlib.sha256(password_with_salt.encode()).hexdigest()
        
        # Retourner salt:hash
        return f"{salt}:{password_hash}"
    
    def _verify_password(self, password: str, password_hash: str) -> bool:
        """
        Vérifie un mot de passe contre son hash.
        
        Args:
            password: Mot de passe en clair
            password_hash: Hash du mot de passe (format: salt:hash)
        
        Returns:
            True si le mot de passe correspond, False sinon
        """
        try:
            salt, stored_hash = password_hash.split(":", 1)
            
            # Hasher le mot de passe fourni avec le même salt
            password_with_salt = f"{salt}{password}"
            computed_hash = hashlib.sha256(password_with_salt.encode()).hexdigest()
            
            return computed_hash == stored_hash
        
        except ValueError:
            logger.error("Format de hash invalide")
            return False
