"""
RulesManager - Gestion des règles de codage configurables.

Ce module implémente le RulesManager qui permet de charger et gérer des règles
de codage spécifiques à l'établissement depuis des fichiers YAML/JSON.

Exigences : 20.1, 20.2, 20.3
"""

import hashlib
import json
import logging
from datetime import datetime
from pathlib import Path
from typing import Any, Dict, List, Optional

import yaml
from pydantic import BaseModel, Field, field_validator

logger = logging.getLogger(__name__)


class CodingRule(BaseModel):
    """Règle de codage individuelle."""

    rule_id: str = Field(..., description="Identifiant unique de la règle")
    name: str = Field(..., description="Nom de la règle")
    description: str = Field(..., description="Description de la règle")
    category: str = Field(..., description="Catégorie (dp, das, ccam, validation)")
    condition: Dict[str, Any] = Field(..., description="Condition d'application")
    action: str = Field(..., description="Action à effectuer")
    severity: str = Field(default="info", description="Sévérité (bloquant, à_revoir, info)")
    enabled: bool = Field(default=True, description="Règle activée")
    
    @field_validator("severity")
    @classmethod
    def validate_severity(cls, v: str) -> str:
        """Valide la sévérité."""
        valid_severities = ["bloquant", "à_revoir", "info"]
        if v not in valid_severities:
            raise ValueError(f"Sévérité invalide: {v}. Doit être l'une de {valid_severities}")
        return v
    
    @field_validator("category")
    @classmethod
    def validate_category(cls, v: str) -> str:
        """Valide la catégorie."""
        valid_categories = ["dp", "das", "ccam", "validation", "general"]
        if v not in valid_categories:
            raise ValueError(f"Catégorie invalide: {v}. Doit être l'une de {valid_categories}")
        return v


class RuleSet(BaseModel):
    """Ensemble de règles avec métadonnées."""

    version: str = Field(..., description="Version du jeu de règles")
    name: str = Field(..., description="Nom du jeu de règles")
    description: str = Field(default="", description="Description du jeu de règles")
    mode: str = Field(default="conservateur", description="Mode de codage")
    created_date: datetime = Field(default_factory=datetime.utcnow, description="Date de création")
    rules: List[CodingRule] = Field(default_factory=list, description="Liste des règles")
    
    @field_validator("mode")
    @classmethod
    def validate_mode(cls, v: str) -> str:
        """Valide le mode."""
        valid_modes = ["conservateur", "agressif", "equilibre"]
        if v not in valid_modes:
            raise ValueError(f"Mode invalide: {v}. Doit être l'un de {valid_modes}")
        return v


class RulesManager:
    """
    Gestionnaire de règles de codage configurables.
    
    Permet de charger, valider et gérer des règles de codage spécifiques
    à l'établissement depuis des fichiers YAML ou JSON.
    
    Exigences : 20.1, 20.2, 20.3
    """
    
    def __init__(self, rules_dir: Optional[Path] = None):
        """
        Initialise le RulesManager.
        
        Args:
            rules_dir: Répertoire contenant les fichiers de règles (optionnel)
        """
        self.rules_dir = rules_dir or Path("config/rules")
        self.current_ruleset: Optional[RuleSet] = None
        self.rules_hash: Optional[str] = None
        
        logger.info(f"RulesManager initialisé avec rules_dir={self.rules_dir}")
    
    def load_rules(self, file_path: Path) -> RuleSet:
        """
        Charge un jeu de règles depuis un fichier YAML ou JSON.
        
        Args:
            file_path: Chemin vers le fichier de règles
            
        Returns:
            RuleSet chargé et validé
            
        Raises:
            FileNotFoundError: Si le fichier n'existe pas
            ValueError: Si le format est invalide
            
        Exigences : 20.1
        """
        if not file_path.exists():
            raise FileNotFoundError(f"Fichier de règles non trouvé: {file_path}")
        
        logger.info(f"Chargement des règles depuis {file_path}")
        
        # Lire le contenu du fichier
        content = file_path.read_text(encoding="utf-8")
        
        # Déterminer le format (YAML ou JSON)
        if file_path.suffix in [".yaml", ".yml"]:
            data = yaml.safe_load(content)
        elif file_path.suffix == ".json":
            data = json.loads(content)
        else:
            raise ValueError(f"Format de fichier non supporté: {file_path.suffix}")
        
        # Valider et créer le RuleSet
        ruleset = RuleSet(**data)
        
        # Générer le hash
        self.rules_hash = self._generate_hash(content)
        
        # Stocker le ruleset courant
        self.current_ruleset = ruleset
        
        logger.info(
            f"Règles chargées: version={ruleset.version}, "
            f"mode={ruleset.mode}, "
            f"{len(ruleset.rules)} règles, "
            f"hash={self.rules_hash[:16]}..."
        )
        
        return ruleset
    
    def _generate_hash(self, content: str) -> str:
        """
        Génère un hash SHA-256 du contenu des règles.
        
        Args:
            content: Contenu du fichier de règles
            
        Returns:
            Hash SHA-256 en hexadécimal
            
        Exigences : 20.2
        """
        return hashlib.sha256(content.encode("utf-8")).hexdigest()
    
    def get_rules_by_category(self, category: str) -> List[CodingRule]:
        """
        Récupère les règles d'une catégorie spécifique.
        
        Args:
            category: Catégorie de règles (dp, das, ccam, validation, general)
            
        Returns:
            Liste des règles de la catégorie
        """
        if not self.current_ruleset:
            return []
        
        return [
            rule for rule in self.current_ruleset.rules
            if rule.category == category and rule.enabled
        ]
    
    def get_rule_by_id(self, rule_id: str) -> Optional[CodingRule]:
        """
        Récupère une règle par son identifiant.
        
        Args:
            rule_id: Identifiant de la règle
            
        Returns:
            Règle trouvée ou None
        """
        if not self.current_ruleset:
            return None
        
        for rule in self.current_ruleset.rules:
            if rule.rule_id == rule_id:
                return rule
        
        return None
    
    def is_conservative_mode(self) -> bool:
        """
        Vérifie si le mode conservateur est activé.
        
        Returns:
            True si mode conservateur
            
        Exigences : 20.3
        """
        if not self.current_ruleset:
            return True  # Par défaut, mode conservateur
        
        return self.current_ruleset.mode == "conservateur"
    
    def is_aggressive_mode(self) -> bool:
        """
        Vérifie si le mode agressif est activé.
        
        Returns:
            True si mode agressif
            
        Exigences : 20.3
        """
        if not self.current_ruleset:
            return False
        
        return self.current_ruleset.mode == "agressif"
    
    def get_version_info(self) -> Dict[str, Any]:
        """
        Récupère les informations de version du jeu de règles.
        
        Returns:
            Dictionnaire avec version, hash, mode, etc.
            
        Exigences : 20.2
        """
        if not self.current_ruleset:
            return {
                "version": "none",
                "hash": "0" * 64,
                "mode": "conservateur",
                "rules_count": 0,
            }
        
        return {
            "version": self.current_ruleset.version,
            "hash": self.rules_hash,
            "mode": self.current_ruleset.mode,
            "rules_count": len(self.current_ruleset.rules),
            "enabled_rules_count": len([r for r in self.current_ruleset.rules if r.enabled]),
            "created_date": self.current_ruleset.created_date.isoformat(),
        }
    
    def apply_rule(self, rule: CodingRule, context: Dict[str, Any]) -> bool:
        """
        Évalue si une règle s'applique dans un contexte donné.
        
        Args:
            rule: Règle à évaluer
            context: Contexte d'application (faits, codes, etc.)
            
        Returns:
            True si la règle s'applique
        """
        # Implémentation simplifiée - à étendre selon les besoins
        # Pour l'instant, on vérifie juste si la règle est activée
        return rule.enabled
    
    def save_rules(self, file_path: Path, format: str = "yaml") -> None:
        """
        Sauvegarde le jeu de règles courant dans un fichier.
        
        Args:
            file_path: Chemin de destination
            format: Format de sortie ("yaml" ou "json")
            
        Raises:
            ValueError: Si aucun ruleset n'est chargé
        """
        if not self.current_ruleset:
            raise ValueError("Aucun jeu de règles chargé")
        
        # Convertir en dictionnaire
        data = self.current_ruleset.model_dump(mode="json")
        
        # Sauvegarder selon le format
        if format == "yaml":
            content = yaml.dump(data, default_flow_style=False, allow_unicode=True)
        elif format == "json":
            content = json.dumps(data, indent=2, ensure_ascii=False)
        else:
            raise ValueError(f"Format non supporté: {format}")
        
        file_path.write_text(content, encoding="utf-8")
        
        logger.info(f"Règles sauvegardées dans {file_path}")
    
    def create_default_ruleset(self) -> RuleSet:
        """
        Crée un jeu de règles par défaut.
        
        Returns:
            RuleSet par défaut avec règles de base
        """
        default_rules = [
            CodingRule(
                rule_id="dp_001",
                name="DP obligatoire",
                description="Un diagnostic principal doit toujours être présent",
                category="dp",
                condition={"type": "required"},
                action="reject_if_missing",
                severity="bloquant",
            ),
            CodingRule(
                rule_id="dp_002",
                name="DP avec preuves",
                description="Le DP doit avoir au moins une preuve",
                category="dp",
                condition={"min_evidence": 1},
                action="reject_if_insufficient",
                severity="bloquant",
            ),
            CodingRule(
                rule_id="neg_001",
                name="Pas de codes pour faits niés",
                description="Les faits niés ne doivent pas être codés",
                category="validation",
                condition={"qualifier": "nié"},
                action="reject_code",
                severity="bloquant",
            ),
            CodingRule(
                rule_id="susp_001",
                name="Pas de DP pour faits suspectés",
                description="Les faits suspectés ne peuvent pas être DP",
                category="dp",
                condition={"qualifier": "suspecté"},
                action="reject_as_dp",
                severity="bloquant",
            ),
            CodingRule(
                rule_id="ccam_001",
                name="Date CCAM obligatoire",
                description="Chaque acte CCAM doit avoir une date de réalisation",
                category="ccam",
                condition={"type": "required_date"},
                action="reject_if_missing",
                severity="bloquant",
            ),
        ]
        
        ruleset = RuleSet(
            version="1.0.0",
            name="Règles par défaut",
            description="Jeu de règles de base pour le codage PMSI",
            mode="conservateur",
            rules=default_rules,
        )
        
        self.current_ruleset = ruleset
        
        # Générer le hash
        content = json.dumps(ruleset.model_dump(mode="json"), sort_keys=True)
        self.rules_hash = self._generate_hash(content)
        
        logger.info(f"Jeu de règles par défaut créé: {len(default_rules)} règles")
        
        return ruleset
