"""
Modèles de données pour RPA Vision V2
Contient les dataclasses pour TaskProfile, Action et Detection
"""

from dataclasses import dataclass, field, asdict
from datetime import datetime
from typing import List, Dict, Any, Tuple, Optional
import json
import numpy as np


@dataclass
class Detection:
    """
    Représente une détection d'élément UI par un modèle de vision
    
    Attributes:
        label: Nom/label de l'élément détecté
        confidence: Score de confiance de la détection (0-1)
        bbox: Bounding box (x, y, width, height) en pixels
        embedding: Embedding visuel 512-d de l'élément
        model_source: Modèle ayant effectué la détection ("owl-v2", "dino", "yolo")
        roi_image: Image de la région d'intérêt (optionnel)
        metadata: Métadonnées additionnelles du modèle
    """
    label: str
    confidence: float
    bbox: Tuple[int, int, int, int]
    embedding: np.ndarray
    model_source: str
    roi_image: Optional[np.ndarray] = None
    metadata: Dict[str, Any] = field(default_factory=dict)
    
    def to_dict(self) -> Dict[str, Any]:
        """
        Convertit la détection en dictionnaire pour sérialisation
        Note: Les arrays numpy ne sont pas sérialisés directement
        """
        return {
            "label": self.label,
            "confidence": float(self.confidence),
            "bbox": list(self.bbox),
            "model_source": self.model_source,
            "metadata": self.metadata,
            # embedding et roi_image sont exclus car non JSON-sérialisables
        }
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any], embedding: Optional[np.ndarray] = None) -> 'Detection':
        """
        Crée une instance Detection depuis un dictionnaire
        
        Args:
            data: Dictionnaire contenant les données de détection
            embedding: Embedding numpy (doit être fourni séparément)
        """
        return cls(
            label=data["label"],
            confidence=data["confidence"],
            bbox=tuple(data["bbox"]),
            embedding=embedding if embedding is not None else np.array([]),
            model_source=data["model_source"],
            metadata=data.get("metadata", {})
        )


@dataclass
class Action:
    """
    Représente une action UI effectuée ou suggérée
    
    Attributes:
        action_type: Type d'action ("click", "type", "scroll", "wait")
        target_element: Nom de l'élément cible
        bbox: Bounding box de l'élément cible
        confidence: Score de confiance pour cette action
        embedding: Embedding visuel de l'élément cible
        timestamp: Horodatage de l'action
        window_title: Titre de la fenêtre où l'action est effectuée
        parameters: Paramètres additionnels (ex: texte à taper, direction de scroll)
        result: Résultat de l'exécution ("success", "failed", "pending")
    """
    action_type: str
    target_element: str
    bbox: Tuple[int, int, int, int]
    confidence: float
    embedding: np.ndarray
    timestamp: datetime
    window_title: str
    parameters: Dict[str, Any] = field(default_factory=dict)
    result: str = "pending"
    
    def to_dict(self) -> Dict[str, Any]:
        """
        Convertit l'action en dictionnaire pour sérialisation
        """
        return {
            "action_type": self.action_type,
            "target_element": self.target_element,
            "bbox": list(self.bbox),
            "confidence": float(self.confidence),
            "timestamp": self.timestamp.isoformat(),
            "window_title": self.window_title,
            "parameters": self.parameters,
            "result": self.result,
        }
    
    @classmethod
    def from_dict(cls, data: Dict[str, Any], embedding: Optional[np.ndarray] = None) -> 'Action':
        """
        Crée une instance Action depuis un dictionnaire
        
        Args:
            data: Dictionnaire contenant les données d'action
            embedding: Embedding numpy (doit être fourni séparément)
        """
        return cls(
            action_type=data["action_type"],
            target_element=data["target_element"],
            bbox=tuple(data["bbox"]),
            confidence=data["confidence"],
            embedding=embedding if embedding is not None else np.array([]),
            timestamp=datetime.fromisoformat(data["timestamp"]),
            window_title=data["window_title"],
            parameters=data.get("parameters", {}),
            result=data.get("result", "pending"),
        )
    
    def get_inverse_action(self) -> Optional['Action']:
        """
        Retourne l'action inverse pour rollback (si applicable)
        """
        # Cette méthode sera implémentée plus tard dans input_utils
        # Pour l'instant, retourne None
        return None


@dataclass
class TaskProfile:
    """
    Profil d'une tâche apprise par le système
    
    Attributes:
        task_id: Identifiant unique de la tâche
        task_name: Nom descriptif de la tâche
        mode: Mode opérationnel actuel ("shadow", "assist", "auto")
        observation_count: Nombre d'observations de cette tâche
        concordance_rate: Taux de concordance (0-1)
        confidence_score: Score de confiance global (0-1)
        correction_count: Nombre de corrections reçues
        last_execution: Horodatage de la dernière exécution
        window_whitelist: Liste des fenêtres autorisées pour cette tâche
        action_sequence: Séquence d'actions composant la tâche
        embeddings: Liste des embeddings visuels associés
        metadata: Métadonnées additionnelles
        execution_history: Historique des exécutions récentes
    """
    task_id: str
    task_name: str
    mode: str = "shadow"
    observation_count: int = 0
    concordance_rate: float = 0.0
    confidence_score: float = 0.0
    correction_count: int = 0
    last_execution: Optional[datetime] = None
    window_whitelist: List[str] = field(default_factory=list)
    action_sequence: List[Action] = field(default_factory=list)
    embeddings: List[np.ndarray] = field(default_factory=list)
    metadata: Dict[str, Any] = field(default_factory=dict)
    execution_history: List[Dict[str, Any]] = field(default_factory=list)
    
    def to_json(self) -> str:
        """
        Sérialise le profil de tâche en JSON
        Note: Les embeddings numpy ne sont pas inclus dans le JSON
        """
        data = {
            "task_id": self.task_id,
            "task_name": self.task_name,
            "mode": self.mode,
            "observation_count": self.observation_count,
            "concordance_rate": float(self.concordance_rate),
            "confidence_score": float(self.confidence_score),
            "correction_count": self.correction_count,
            "last_execution": self.last_execution.isoformat() if self.last_execution else None,
            "window_whitelist": self.window_whitelist,
            "action_sequence": [action.to_dict() for action in self.action_sequence],
            "metadata": self.metadata,
            "execution_history": self.execution_history,
        }
        return json.dumps(data, indent=2, ensure_ascii=False)
    
    @classmethod
    def from_json(cls, json_str: str, embeddings: Optional[List[np.ndarray]] = None) -> 'TaskProfile':
        """
        Crée une instance TaskProfile depuis une chaîne JSON
        
        Args:
            json_str: Chaîne JSON contenant les données du profil
            embeddings: Liste d'embeddings numpy (doivent être fournis séparément)
        """
        data = json.loads(json_str)
        
        # Reconstruire les actions
        actions = [Action.from_dict(action_data) for action_data in data.get("action_sequence", [])]
        
        return cls(
            task_id=data["task_id"],
            task_name=data["task_name"],
            mode=data.get("mode", "shadow"),
            observation_count=data.get("observation_count", 0),
            concordance_rate=data.get("concordance_rate", 0.0),
            confidence_score=data.get("confidence_score", 0.0),
            correction_count=data.get("correction_count", 0),
            last_execution=datetime.fromisoformat(data["last_execution"]) if data.get("last_execution") else None,
            window_whitelist=data.get("window_whitelist", []),
            action_sequence=actions,
            embeddings=embeddings if embeddings is not None else [],
            metadata=data.get("metadata", {}),
            execution_history=data.get("execution_history", []),
        )
    
    def get_historical_performance(self) -> float:
        """
        Calcule la performance historique basée sur les exécutions récentes
        
        Returns:
            Score de performance (0-1)
        """
        if not self.execution_history:
            return 0.0
        
        # Calculer le taux de succès sur les exécutions récentes
        recent_executions = self.execution_history[-10:]  # 10 dernières exécutions
        success_count = sum(1 for exec in recent_executions if exec.get("result") == "success")
        
        return success_count / len(recent_executions) if recent_executions else 0.0
    
    def add_execution(self, result: str, confidence: float, latency_ms: float):
        """
        Ajoute une exécution à l'historique
        
        Args:
            result: Résultat de l'exécution ("success", "failed")
            confidence: Score de confiance de l'exécution
            latency_ms: Latence en millisecondes
        """
        execution = {
            "timestamp": datetime.now().isoformat(),
            "result": result,
            "confidence": confidence,
            "latency_ms": latency_ms,
        }
        self.execution_history.append(execution)
        self.last_execution = datetime.now()
        
        # Limiter l'historique aux 50 dernières exécutions
        if len(self.execution_history) > 50:
            self.execution_history = self.execution_history[-50:]
    
    def update_concordance_rate(self, success: bool):
        """
        Met à jour le taux de concordance basé sur le résultat d'une exécution
        
        Args:
            success: True si l'exécution a réussi, False sinon
        """
        # Utiliser une moyenne mobile pour le taux de concordance
        window_size = 10  # Fenêtre de 10 exécutions
        recent_executions = self.execution_history[-window_size:]
        
        if recent_executions:
            success_count = sum(1 for exec in recent_executions if exec.get("result") == "success")
            self.concordance_rate = success_count / len(recent_executions)
    
    def should_transition_to_auto(self, min_observations: int = 20, min_concordance: float = 0.95) -> bool:
        """
        Vérifie si la tâche remplit les critères pour passer en mode Autopilot
        
        Args:
            min_observations: Nombre minimum d'observations requises
            min_concordance: Taux de concordance minimum requis
            
        Returns:
            True si les critères sont remplis
        """
        return (self.observation_count >= min_observations and 
                self.concordance_rate >= min_concordance)
    
    def should_rollback_to_assist(self, min_confidence: float = 0.90) -> bool:
        """
        Vérifie si la tâche doit être rétrogradée au mode Assisté
        
        Args:
            min_confidence: Score de confiance minimum requis
            
        Returns:
            True si la confiance est trop faible
        """
        return self.mode == "auto" and self.confidence_score < min_confidence


if __name__ == "__main__":
    # Tests basiques des modèles
    print("Test des modèles de données RPA Vision V2")
    print("=" * 50)
    
    # Test Detection
    print("\n1. Test Detection:")
    detection = Detection(
        label="valider_button",
        confidence=0.93,
        bbox=(450, 320, 120, 40),
        embedding=np.random.rand(512),
        model_source="owl-v2",
        metadata={"class": "button"}
    )
    print(f"   Label: {detection.label}")
    print(f"   Confidence: {detection.confidence}")
    print(f"   BBox: {detection.bbox}")
    print(f"   Model: {detection.model_source}")
    det_dict = detection.to_dict()
    print(f"   Dict keys: {list(det_dict.keys())}")
    
    # Test Action
    print("\n2. Test Action:")
    action = Action(
        action_type="click",
        target_element="valider_button",
        bbox=(450, 320, 120, 40),
        confidence=0.95,
        embedding=np.random.rand(512),
        timestamp=datetime.now(),
        window_title="Dolibarr - Facturation",
        parameters={"button": "left"},
        result="success"
    )
    print(f"   Type: {action.action_type}")
    print(f"   Target: {action.target_element}")
    print(f"   Window: {action.window_title}")
    print(f"   Result: {action.result}")
    action_dict = action.to_dict()
    print(f"   Dict keys: {list(action_dict.keys())}")
    
    # Test TaskProfile
    print("\n3. Test TaskProfile:")
    task = TaskProfile(
        task_id="ouvrir_facture_001",
        task_name="Ouvrir Facture",
        mode="assist",
        observation_count=15,
        concordance_rate=0.87,
        confidence_score=0.92,
        window_whitelist=["Dolibarr - Facturation"],
        action_sequence=[action]
    )
    print(f"   Task ID: {task.task_id}")
    print(f"   Mode: {task.mode}")
    print(f"   Observations: {task.observation_count}")
    print(f"   Concordance: {task.concordance_rate:.2%}")
    print(f"   Confidence: {task.confidence_score:.2%}")
    
    # Test sérialisation JSON
    print("\n4. Test sérialisation JSON:")
    json_str = task.to_json()
    print(f"   JSON length: {len(json_str)} chars")
    
    # Test désérialisation
    task_restored = TaskProfile.from_json(json_str)
    print(f"   Restored task ID: {task_restored.task_id}")
    print(f"   Restored mode: {task_restored.mode}")
    
    # Test méthodes de transition
    print("\n5. Test méthodes de transition:")
    print(f"   Should transition to auto: {task.should_transition_to_auto()}")
    print(f"   Should rollback to assist: {task.should_rollback_to_assist()}")
    
    print("\n✓ Tous les tests basiques réussis!")
