"""
Détecteur de changements UI pour RPA Vision V2.
Détecte les dérives d'interface et déclenche le ré-entraînement si nécessaire.
"""

import numpy as np
from typing import Dict, List, Optional, Any, Tuple, TYPE_CHECKING
from datetime import datetime

if TYPE_CHECKING:
    from .embeddings_manager import EmbeddingsManager
    from .logger import Logger


class UIChangeDetector:
    """
    Détecteur de changements UI qui surveille les dérives d'interface
    et déclenche le ré-entraînement lorsque nécessaire.
    """
    
    def __init__(
        self,
        embeddings_manager: "EmbeddingsManager",
        logger: "Logger",
        config: Dict[str, Any]
    ):
        """
        Initialise le détecteur de changements UI.
        
        Args:
            embeddings_manager: Gestionnaire d'embeddings pour comparaison
            logger: Logger pour la journalisation
            config: Configuration globale
        """
        self.embeddings_manager = embeddings_manager
        self.logger = logger
        self.config = config
        
        # Seuils de configuration
        self.ui_change_threshold = config.get("thresholds", {}).get(
            "ui_change_similarity", 0.70
        )
        self.bbox_delta_threshold = config.get("thresholds", {}).get(
            "bbox_delta_pixels", 10
        )
        
        # Historique des changements détectés
        self.change_history: List[Dict[str, Any]] = []
        
        self.logger.log_action({
            "action": "ui_change_detector_initialized",
            "ui_change_threshold": self.ui_change_threshold,
            "bbox_delta_threshold": self.bbox_delta_threshold
        })
    
    def detect_ui_change(
        self,
        current_embedding: np.ndarray,
        stored_embeddings: List[np.ndarray],
        task_id: str
    ) -> Tuple[bool, float]:
        """
        Détecte si l'UI a changé en comparant l'embedding actuel
        avec les embeddings stockés.
        
        Args:
            current_embedding: Embedding visuel actuel de l'élément UI
            stored_embeddings: Liste des embeddings stockés pour cette tâche
            task_id: ID de la tâche concernée
            
        Returns:
            Tuple (changement_détecté, similarité_max)
            - changement_détecté: True si similarité < seuil (70%)
            - similarité_max: Meilleure similarité trouvée
        """
        if not stored_embeddings or len(stored_embeddings) == 0:
            self.logger.log_action({
                "action": "ui_change_check_skipped",
                "task_id": task_id,
                "reason": "no_stored_embeddings"
            })
            return False, 0.0
        
        try:
            # Calculer la similarité avec tous les embeddings stockés
            similarities = []
            for stored_emb in stored_embeddings:
                similarity = self.embeddings_manager.get_embedding_similarity(
                    current_embedding,
                    stored_emb
                )
                similarities.append(similarity)
            
            # Prendre la meilleure similarité
            max_similarity = max(similarities)
            avg_similarity = np.mean(similarities)
            
            # Détecter le changement si la similarité est inférieure au seuil
            change_detected = max_similarity < self.ui_change_threshold
            
            # Logger le résultat
            self.logger.log_action({
                "action": "ui_change_detected" if change_detected else "ui_stable",
                "task_id": task_id,
                "max_similarity": float(max_similarity),
                "avg_similarity": float(avg_similarity),
                "threshold": self.ui_change_threshold,
                "num_stored_embeddings": len(stored_embeddings)
            })
            
            # Enregistrer dans l'historique si changement détecté
            if change_detected:
                self.change_history.append({
                    "timestamp": datetime.now().isoformat(),
                    "task_id": task_id,
                    "max_similarity": float(max_similarity),
                    "avg_similarity": float(avg_similarity),
                    "threshold": self.ui_change_threshold
                })
            
            return change_detected, max_similarity
        
        except Exception as e:
            self.logger.log_action({
                "action": "ui_change_detection_error",
                "task_id": task_id,
                "error": str(e)
            })
            # En cas d'erreur, considérer qu'il n'y a pas de changement
            return False, 0.0
    
    def calculate_delta(
        self,
        predicted_bbox: Tuple[int, int, int, int],
        actual_bbox: Tuple[int, int, int, int]
    ) -> Dict[str, float]:
        """
        Calcule le delta en pixels entre la bbox prédite et la bbox réelle.
        
        Args:
            predicted_bbox: Bounding box prédite (x, y, width, height)
            actual_bbox: Bounding box réelle (x, y, width, height)
            
        Returns:
            Dictionnaire avec les deltas:
            - delta_x: Différence en x (pixels)
            - delta_y: Différence en y (pixels)
            - delta_width: Différence en largeur (pixels)
            - delta_height: Différence en hauteur (pixels)
            - delta_center: Distance euclidienne entre les centres (pixels)
            - max_delta: Delta maximum parmi x, y
        """
        try:
            pred_x, pred_y, pred_w, pred_h = predicted_bbox
            actual_x, actual_y, actual_w, actual_h = actual_bbox
            
            # Calculer les deltas
            delta_x = abs(actual_x - pred_x)
            delta_y = abs(actual_y - pred_y)
            delta_width = abs(actual_w - pred_w)
            delta_height = abs(actual_h - pred_h)
            
            # Calculer les centres
            pred_center_x = pred_x + pred_w / 2
            pred_center_y = pred_y + pred_h / 2
            actual_center_x = actual_x + actual_w / 2
            actual_center_y = actual_y + actual_h / 2
            
            # Distance euclidienne entre les centres
            delta_center = np.sqrt(
                (actual_center_x - pred_center_x) ** 2 +
                (actual_center_y - pred_center_y) ** 2
            )
            
            # Delta maximum (position)
            max_delta = max(delta_x, delta_y)
            
            deltas = {
                "delta_x": float(delta_x),
                "delta_y": float(delta_y),
                "delta_width": float(delta_width),
                "delta_height": float(delta_height),
                "delta_center": float(delta_center),
                "max_delta": float(max_delta)
            }
            
            self.logger.log_action({
                "action": "bbox_delta_calculated",
                "predicted_bbox": list(predicted_bbox),
                "actual_bbox": list(actual_bbox),
                "deltas": deltas
            })
            
            return deltas
        
        except Exception as e:
            self.logger.log_action({
                "action": "bbox_delta_calculation_error",
                "error": str(e)
            })
            return {
                "delta_x": 0.0,
                "delta_y": 0.0,
                "delta_width": 0.0,
                "delta_height": 0.0,
                "delta_center": 0.0,
                "max_delta": 0.0
            }
    
    def should_trigger_retraining(
        self,
        deltas: Dict[str, float],
        similarity: float
    ) -> bool:
        """
        Détermine si le ré-entraînement doit être déclenché.
        
        Args:
            deltas: Dictionnaire des deltas de bbox
            similarity: Similarité d'embedding
            
        Returns:
            True si ré-entraînement nécessaire
        """
        # Déclencher si delta de position > seuil OU similarité < seuil
        position_drift = deltas.get("max_delta", 0) > self.bbox_delta_threshold
        visual_drift = similarity < self.ui_change_threshold
        
        should_retrain = position_drift or visual_drift
        
        self.logger.log_action({
            "action": "retraining_decision",
            "should_retrain": should_retrain,
            "position_drift": position_drift,
            "visual_drift": visual_drift,
            "max_delta": deltas.get("max_delta", 0),
            "similarity": float(similarity),
            "bbox_threshold": self.bbox_delta_threshold,
            "similarity_threshold": self.ui_change_threshold
        })
        
        return should_retrain
    
    def trigger_retraining(
        self,
        task_id: str,
        reason: str,
        metadata: Optional[Dict[str, Any]] = None
    ):
        """
        Déclenche le ré-entraînement pour une tâche spécifique.
        
        Args:
            task_id: ID de la tâche nécessitant un ré-entraînement
            reason: Raison du ré-entraînement
            metadata: Métadonnées additionnelles
        """
        retraining_event = {
            "timestamp": datetime.now().isoformat(),
            "task_id": task_id,
            "reason": reason,
            "metadata": metadata or {}
        }
        
        self.logger.log_action({
            "action": "retraining_triggered",
            **retraining_event
        })
        
        # Enregistrer dans l'historique
        self.change_history.append(retraining_event)
        
        # Note: L'implémentation réelle du ré-entraînement sera gérée
        # par le LearningManager qui recevra cette notification
    
    def check_and_trigger_retraining(
        self,
        task_id: str,
        current_embedding: np.ndarray,
        stored_embeddings: List[np.ndarray],
        predicted_bbox: Optional[Tuple[int, int, int, int]] = None,
        actual_bbox: Optional[Tuple[int, int, int, int]] = None
    ) -> Dict[str, Any]:
        """
        Vérifie les changements UI et déclenche le ré-entraînement si nécessaire.
        
        Args:
            task_id: ID de la tâche
            current_embedding: Embedding actuel
            stored_embeddings: Embeddings stockés
            predicted_bbox: Bbox prédite (optionnel)
            actual_bbox: Bbox réelle (optionnel)
            
        Returns:
            Dictionnaire avec les résultats de la vérification
        """
        result = {
            "task_id": task_id,
            "ui_change_detected": False,
            "position_drift_detected": False,
            "retraining_triggered": False,
            "similarity": 0.0,
            "deltas": {}
        }
        
        # Vérifier les changements visuels
        ui_changed, similarity = self.detect_ui_change(
            current_embedding,
            stored_embeddings,
            task_id
        )
        result["ui_change_detected"] = ui_changed
        result["similarity"] = similarity
        
        # Vérifier les deltas de position si disponibles
        if predicted_bbox is not None and actual_bbox is not None:
            deltas = self.calculate_delta(predicted_bbox, actual_bbox)
            result["deltas"] = deltas
            result["position_drift_detected"] = (
                deltas.get("max_delta", 0) > self.bbox_delta_threshold
            )
        else:
            deltas = {}
        
        # Décider si ré-entraînement nécessaire
        if self.should_trigger_retraining(deltas, similarity):
            reasons = []
            if ui_changed:
                reasons.append(f"visual_drift (similarity={similarity:.2f})")
            if result["position_drift_detected"]:
                reasons.append(f"position_drift (delta={deltas.get('max_delta', 0):.1f}px)")
            
            reason = ", ".join(reasons)
            self.trigger_retraining(
                task_id,
                reason,
                {
                    "similarity": similarity,
                    "deltas": deltas,
                    "ui_change_detected": ui_changed,
                    "position_drift_detected": result["position_drift_detected"]
                }
            )
            result["retraining_triggered"] = True
        
        return result
    
    def get_change_history(
        self,
        task_id: Optional[str] = None,
        limit: int = 50
    ) -> List[Dict[str, Any]]:
        """
        Retourne l'historique des changements détectés.
        
        Args:
            task_id: Filtrer par task_id (optionnel)
            limit: Nombre maximum d'entrées à retourner
            
        Returns:
            Liste des changements détectés
        """
        history = self.change_history
        
        # Filtrer par task_id si spécifié
        if task_id:
            history = [h for h in history if h.get("task_id") == task_id]
        
        # Limiter le nombre de résultats
        return history[-limit:]
    
    def get_stats(self) -> Dict[str, Any]:
        """
        Retourne des statistiques sur les changements détectés.
        
        Returns:
            Dictionnaire de statistiques
        """
        total_changes = len(self.change_history)
        
        # Compter les changements par tâche
        changes_by_task = {}
        for change in self.change_history:
            task_id = change.get("task_id", "unknown")
            changes_by_task[task_id] = changes_by_task.get(task_id, 0) + 1
        
        # Compter les ré-entraînements déclenchés
        retraining_count = sum(
            1 for change in self.change_history
            if change.get("reason") is not None
        )
        
        return {
            "total_changes_detected": total_changes,
            "retraining_triggered_count": retraining_count,
            "changes_by_task": changes_by_task,
            "ui_change_threshold": self.ui_change_threshold,
            "bbox_delta_threshold": self.bbox_delta_threshold
        }
    
    def clear_history(self):
        """Efface l'historique des changements."""
        self.change_history = []
        self.logger.log_action({
            "action": "change_history_cleared"
        })
