"""
Détecteur d'éléments UI pour la Phase 2 - Mode Enrichi.
Implémente le pipeline complet de détection d'éléments UI.

Pipeline:
1. RegionProposer - Propose des régions d'intérêt candidates
2. ElementCharacterizer - Caractérise chaque région
3. ElementClassifier - Classifie type et rôle
"""

import cv2
import numpy as np
from typing import List, Dict, Any, Tuple, Optional
from dataclasses import dataclass
from pathlib import Path

from .ui_element_models import (
    UIElement,
    UIElementType,
    VisualData,
    TextData,
    ElementProperties,
    ElementContext,
    WindowInfo
)
from .llm_manager import LLMManager
from .logger import Logger


@dataclass
class BoundingBox:
    """Représente une bounding box."""
    x1: int
    y1: int
    x2: int
    y2: int
    confidence: float = 1.0
    source: str = "unknown"
    
    def area(self) -> int:
        """Calcule l'aire de la bbox."""
        return (self.x2 - self.x1) * (self.y2 - self.y1)
    
    def center(self) -> Tuple[int, int]:
        """Calcule le centre de la bbox."""
        return ((self.x1 + self.x2) // 2, (self.y1 + self.y2) // 2)
    
    def iou(self, other: 'BoundingBox') -> float:
        """Calcule l'Intersection over Union avec une autre bbox."""
        # Intersection
        x1_inter = max(self.x1, other.x1)
        y1_inter = max(self.y1, other.y1)
        x2_inter = min(self.x2, other.x2)
        y2_inter = min(self.y2, other.y2)
        
        if x2_inter < x1_inter or y2_inter < y1_inter:
            return 0.0
        
        inter_area = (x2_inter - x1_inter) * (y2_inter - y1_inter)
        
        # Union
        union_area = self.area() + other.area() - inter_area
        
        return inter_area / union_area if union_area > 0 else 0.0


class RegionProposer:
    """
    Propose des régions d'intérêt candidates pour les éléments UI.
    
    Méthodes:
    1. Détection de zones de texte (rapide)
    2. Détection de rectangles autour de texte (heuristique)
    3. Requête VLM pour zones cliquables (optionnel, coûteux)
    """
    
    def __init__(
        self,
        llm_manager: Optional[LLMManager] = None,
        logger: Optional[Logger] = None,
        config: Optional[Dict[str, Any]] = None
    ):
        """
        Initialise le RegionProposer.
        
        Args:
            llm_manager: Gestionnaire LLM pour requêtes VLM
            logger: Logger pour journalisation
            config: Configuration
        """
        self.llm = llm_manager
        self.logger = logger
        self.config = config or {}
        
        # Configuration
        self.use_text_detection = self.config.get("use_text_detection", True)
        self.use_rectangle_detection = self.config.get("use_rectangle_detection", True)
        self.use_vlm_detection = self.config.get("use_vlm_detection", False)
        self.min_region_size = self.config.get("min_region_size", 20)
        self.max_region_size = self.config.get("max_region_size", 500)
    
    def propose_regions(
        self,
        screenshot: np.ndarray,
        window_info: WindowInfo
    ) -> List[BoundingBox]:
        """
        Propose des régions d'intérêt candidates.
        
        Args:
            screenshot: Screenshot numpy array
            window_info: Informations sur la fenêtre
            
        Returns:
            Liste de BoundingBox candidates
        """
        regions = []
        
        # Méthode 1: Zones de texte (rapide)
        if self.use_text_detection:
            text_regions = self._detect_text_regions(screenshot)
            regions.extend(text_regions)
            if self.logger:
                self.logger.log_action({
                    "action": "text_regions_detected",
                    "count": len(text_regions)
                })
        
        # Méthode 2: Rectangles propres (heuristique)
        if self.use_rectangle_detection:
            rect_regions = self._detect_rectangles(screenshot)
            regions.extend(rect_regions)
            if self.logger:
                self.logger.log_action({
                    "action": "rectangle_regions_detected",
                    "count": len(rect_regions)
                })
        
        # Méthode 3: VLM (précis mais lent)
        if self.use_vlm_detection and self._should_use_vlm(window_info):
            vlm_regions = self._query_vlm_for_regions(screenshot)
            regions.extend(vlm_regions)
            if self.logger:
                self.logger.log_action({
                    "action": "vlm_regions_detected",
                    "count": len(vlm_regions)
                })
        
        # Fusion et nettoyage
        regions = self._merge_overlapping_regions(regions)
        regions = self._filter_invalid_regions(regions, screenshot.shape)
        
        if self.logger:
            self.logger.log_action({
                "action": "regions_proposed",
                "total_count": len(regions)
            })
        
        return regions
    
    def _detect_text_regions(self, screenshot: np.ndarray) -> List[BoundingBox]:
        """Détecte les zones de texte en utilisant des heuristiques simples."""
        regions = []
        
        try:
            # Convertir en niveaux de gris
            gray = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY)
            
            # Appliquer un seuil adaptatif
            thresh = cv2.adaptiveThreshold(
                gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
                cv2.THRESH_BINARY_INV, 11, 2
            )
            
            # Trouver les contours
            contours, _ = cv2.findContours(
                thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
            )
            
            # Filtrer et créer des bboxes
            for contour in contours:
                x, y, w, h = cv2.boundingRect(contour)
                
                # Filtrer par taille
                if w < self.min_region_size or h < self.min_region_size:
                    continue
                if w > self.max_region_size or h > self.max_region_size:
                    continue
                
                # Filtrer par ratio (texte généralement horizontal)
                ratio = w / h if h > 0 else 0
                if ratio < 0.5 or ratio > 20:
                    continue
                
                regions.append(BoundingBox(
                    x1=x, y1=y, x2=x+w, y2=y+h,
                    confidence=0.7,
                    source="text_detection"
                ))
        
        except Exception as e:
            if self.logger:
                self.logger.log_action({
                    "action": "text_detection_error",
                    "error": str(e)
                })
        
        return regions
    
    def _detect_rectangles(self, screenshot: np.ndarray) -> List[BoundingBox]:
        """Détecte les rectangles propres (boutons, champs, etc.)."""
        regions = []
        
        try:
            # Convertir en niveaux de gris
            gray = cv2.cvtColor(screenshot, cv2.COLOR_BGR2GRAY)
            
            # Détection de contours avec Canny
            edges = cv2.Canny(gray, 50, 150)
            
            # Dilatation pour connecter les contours
            kernel = np.ones((3, 3), np.uint8)
            dilated = cv2.dilate(edges, kernel, iterations=1)
            
            # Trouver les contours
            contours, _ = cv2.findContours(
                dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE
            )
            
            # Filtrer et créer des bboxes
            for contour in contours:
                # Approximer le contour
                epsilon = 0.02 * cv2.arcLength(contour, True)
                approx = cv2.approxPolyDP(contour, epsilon, True)
                
                # Garder seulement les formes rectangulaires (4 coins)
                if len(approx) >= 4:
                    x, y, w, h = cv2.boundingRect(contour)
                    
                    # Filtrer par taille
                    if w < self.min_region_size or h < self.min_region_size:
                        continue
                    if w > self.max_region_size or h > self.max_region_size:
                        continue
                    
                    regions.append(BoundingBox(
                        x1=x, y1=y, x2=x+w, y2=y+h,
                        confidence=0.8,
                        source="rectangle_detection"
                    ))
        
        except Exception as e:
            if self.logger:
                self.logger.log_action({
                    "action": "rectangle_detection_error",
                    "error": str(e)
                })
        
        return regions
    
    def _query_vlm_for_regions(self, screenshot: np.ndarray) -> List[BoundingBox]:
        """Utilise le VLM pour détecter les zones cliquables."""
        regions = []
        
        if not self.llm or not self.llm.is_available():
            return regions
        
        try:
            prompt = """Identifie tous les éléments cliquables dans cette interface (boutons, liens, champs).
Réponds avec une liste de coordonnées au format: x1,y1,x2,y2;x1,y1,x2,y2;..."""
            
            response = self.llm.generate_with_vision(prompt, [screenshot])
            
            # Parser la réponse
            # Format attendu: "x1,y1,x2,y2;x1,y1,x2,y2;..."
            if response and ';' in response:
                for bbox_str in response.split(';'):
                    try:
                        coords = [int(x.strip()) for x in bbox_str.split(',')]
                        if len(coords) == 4:
                            regions.append(BoundingBox(
                                x1=coords[0], y1=coords[1],
                                x2=coords[2], y2=coords[3],
                                confidence=0.9,
                                source="vlm_detection"
                            ))
                    except:
                        continue
        
        except Exception as e:
            if self.logger:
                self.logger.log_action({
                    "action": "vlm_detection_error",
                    "error": str(e)
                })
        
        return regions
    
    def _should_use_vlm(self, window_info: WindowInfo) -> bool:
        """Décide si on doit utiliser le VLM (coûteux)."""
        # Pour l'instant, toujours False sauf si explicitement activé
        return self.use_vlm_detection
    
    def _merge_overlapping_regions(
        self,
        regions: List[BoundingBox],
        iou_threshold: float = 0.5
    ) -> List[BoundingBox]:
        """Fusionne les régions qui se chevauchent."""
        if not regions:
            return []
        
        # Trier par confiance décroissante
        regions = sorted(regions, key=lambda r: r.confidence, reverse=True)
        
        merged = []
        used = set()
        
        for i, region in enumerate(regions):
            if i in used:
                continue
            
            # Chercher les régions qui se chevauchent
            overlapping = [region]
            for j, other in enumerate(regions[i+1:], start=i+1):
                if j in used:
                    continue
                
                if region.iou(other) > iou_threshold:
                    overlapping.append(other)
                    used.add(j)
            
            # Fusionner en prenant l'union
            if len(overlapping) > 1:
                x1 = min(r.x1 for r in overlapping)
                y1 = min(r.y1 for r in overlapping)
                x2 = max(r.x2 for r in overlapping)
                y2 = max(r.y2 for r in overlapping)
                conf = max(r.confidence for r in overlapping)
                
                merged.append(BoundingBox(
                    x1=x1, y1=y1, x2=x2, y2=y2,
                    confidence=conf,
                    source="merged"
                ))
            else:
                merged.append(region)
        
        return merged
    
    def _filter_invalid_regions(
        self,
        regions: List[BoundingBox],
        image_shape: Tuple[int, ...]
    ) -> List[BoundingBox]:
        """Filtre les régions invalides."""
        height, width = image_shape[:2]
        
        valid = []
        for region in regions:
            # Vérifier que la région est dans l'image
            if region.x1 < 0 or region.y1 < 0:
                continue
            if region.x2 > width or region.y2 > height:
                continue
            
            # Vérifier la taille
            w = region.x2 - region.x1
            h = region.y2 - region.y1
            
            if w < self.min_region_size or h < self.min_region_size:
                continue
            if w > self.max_region_size or h > self.max_region_size:
                continue
            
            valid.append(region)
        
        return valid


# À suivre: ElementCharacterizer et ElementClassifier dans la prochaine partie...



class ElementCharacterizer:
    """
    Caractérise chaque région détectée en extrayant ses caractéristiques.
    
    Extrait:
    - Crop image de la région
    - Embedding image via CLIP
    - Texte dans/autour de la région via VLM
    - Embedding texte
    - Position bbox
    """
    
    def __init__(
        self,
        llm_manager: Optional[LLMManager] = None,
        image_embedder: Optional[Any] = None,
        text_embedder: Optional[Any] = None,
        logger: Optional[Logger] = None
    ):
        """
        Initialise l'ElementCharacterizer.
        
        Args:
            llm_manager: Gestionnaire LLM pour extraction de texte
            image_embedder: Embedder d'images (CLIP)
            text_embedder: Embedder de texte
            logger: Logger
        """
        self.llm = llm_manager
        self.image_embedder = image_embedder
        self.text_embedder = text_embedder
        self.logger = logger
    
    def characterize(
        self,
        screenshot: np.ndarray,
        region: BoundingBox,
        window_info: WindowInfo,
        data_dir: str = "data"
    ) -> Optional[Dict[str, Any]]:
        """
        Caractérise une région détectée.
        
        Args:
            screenshot: Screenshot complet
            region: Région à caractériser
            window_info: Informations sur la fenêtre
            data_dir: Répertoire de données
            
        Returns:
            Dictionnaire avec les caractéristiques ou None si échec
        """
        try:
            # Extraire le crop
            crop = screenshot[region.y1:region.y2, region.x1:region.x2]
            
            if crop.size == 0:
                return None
            
            # Sauvegarder le crop (temporaire)
            crop_path = Path(data_dir) / "temp" / f"crop_{region.x1}_{region.y1}.png"
            crop_path.parent.mkdir(parents=True, exist_ok=True)
            cv2.imwrite(str(crop_path), crop)
            
            # Extraire le texte via VLM
            text = self._extract_text(crop)
            
            # Générer les embeddings (simulés pour l'instant)
            image_embedding_path = str(crop_path).replace(".png", "_img_emb.npy")
            text_embedding_path = str(crop_path).replace(".png", "_text_emb.npy")
            
            # Sauvegarder des embeddings factices (à remplacer par vrais embeddings)
            np.save(image_embedding_path, np.random.rand(512))
            np.save(text_embedding_path, np.random.rand(512))
            
            return {
                "crop_path": str(crop_path),
                "crop_image": crop,
                "text_raw": text,
                "text_normalized": text.lower().strip(),
                "bbox": (region.x1, region.y1, region.x2, region.y2),
                "image_embedding_path": image_embedding_path,
                "text_embedding_path": text_embedding_path,
                "confidence": region.confidence,
                "source": region.source
            }
        
        except Exception as e:
            if self.logger:
                self.logger.log_action({
                    "action": "characterization_error",
                    "error": str(e),
                    "region": f"({region.x1},{region.y1},{region.x2},{region.y2})"
                })
            return None
    
    def _extract_text(self, crop: np.ndarray) -> str:
        """Extrait le texte d'un crop via VLM."""
        if not self.llm or not self.llm.is_available():
            return ""
        
        try:
            prompt = "Quel est le texte visible dans cette image? Réponds UNIQUEMENT avec le texte, rien d'autre."
            response = self.llm.generate_with_vision(prompt, [crop])
            return response.strip() if response else ""
        
        except Exception as e:
            if self.logger:
                self.logger.log_action({
                    "action": "text_extraction_error",
                    "error": str(e)
                })
            return ""


class ElementClassifier:
    """
    Classifie le type et le rôle sémantique des éléments.
    
    Utilise:
    - Caractéristiques visuelles
    - Analyse textuelle VLM
    - Heuristiques
    """
    
    def __init__(
        self,
        llm_manager: Optional[LLMManager] = None,
        logger: Optional[Logger] = None,
        config: Optional[Dict[str, Any]] = None
    ):
        """
        Initialise l'ElementClassifier.
        
        Args:
            llm_manager: Gestionnaire LLM
            logger: Logger
            config: Configuration
        """
        self.llm = llm_manager
        self.logger = logger
        self.config = config or {}
    
    def classify(
        self,
        characterized_element: Dict[str, Any],
        window_info: WindowInfo
    ) -> Tuple[UIElementType, str, float]:
        """
        Classifie un élément caractérisé.
        
        Args:
            characterized_element: Élément caractérisé
            window_info: Informations sur la fenêtre
            
        Returns:
            Tuple (type, role, confidence)
        """
        text = characterized_element.get("text_raw", "").lower()
        bbox = characterized_element.get("bbox", (0, 0, 0, 0))
        
        # Classification basée sur le texte et les heuristiques
        element_type, confidence = self._classify_type(text, bbox)
        role = self._infer_role(text, element_type)
        
        return element_type, role, confidence
    
    def _classify_type(
        self,
        text: str,
        bbox: Tuple[int, int, int, int]
    ) -> Tuple[UIElementType, float]:
        """Classifie le type d'élément."""
        # Heuristiques simples basées sur le texte
        text_lower = text.lower()
        
        # Boutons
        button_keywords = ["valider", "ok", "annuler", "enregistrer", "submit", "cancel", "save", "delete", "supprimer"]
        if any(kw in text_lower for kw in button_keywords):
            return UIElementType.BUTTON, 0.8
        
        # Champs de texte
        if "recherche" in text_lower or "search" in text_lower:
            return UIElementType.TEXT_INPUT, 0.7
        
        # Liens
        link_keywords = ["lien", "link", "voir", "view", "plus", "more"]
        if any(kw in text_lower for kw in link_keywords):
            return UIElementType.LINK, 0.6
        
        # Par défaut: élément interactif générique
        return UIElementType.GENERIC_INTERACTIVE, 0.5
    
    def _infer_role(self, text: str, element_type: UIElementType) -> str:
        """Infère le rôle sémantique."""
        text_lower = text.lower()
        
        # Rôles basés sur le texte
        if "valider" in text_lower or "submit" in text_lower:
            return "validate_action"
        elif "annuler" in text_lower or "cancel" in text_lower:
            return "cancel_action"
        elif "enregistrer" in text_lower or "save" in text_lower:
            return "save_action"
        elif "supprimer" in text_lower or "delete" in text_lower:
            return "delete_action"
        elif "recherche" in text_lower or "search" in text_lower:
            return "search_field"
        else:
            return "generic_action"


class UIElementDetector:
    """
    Détecteur principal d'éléments UI.
    Orchestre le pipeline complet: RegionProposer → ElementCharacterizer → ElementClassifier
    """
    
    def __init__(
        self,
        llm_manager: Optional[LLMManager] = None,
        image_embedder: Optional[Any] = None,
        text_embedder: Optional[Any] = None,
        logger: Optional[Logger] = None,
        config: Optional[Dict[str, Any]] = None
    ):
        """
        Initialise le UIElementDetector.
        
        Args:
            llm_manager: Gestionnaire LLM
            image_embedder: Embedder d'images
            text_embedder: Embedder de texte
            logger: Logger
            config: Configuration
        """
        self.llm = llm_manager
        self.logger = logger
        self.config = config or {}
        
        # Créer les composants du pipeline
        self.region_proposer = RegionProposer(
            llm_manager=llm_manager,
            logger=logger,
            config=self.config.get("region_proposer", {})
        )
        
        self.characterizer = ElementCharacterizer(
            llm_manager=llm_manager,
            image_embedder=image_embedder,
            text_embedder=text_embedder,
            logger=logger
        )
        
        self.classifier = ElementClassifier(
            llm_manager=llm_manager,
            logger=logger,
            config=self.config.get("classifier", {})
        )
    
    def detect_elements(
        self,
        screenshot: np.ndarray,
        window_info: WindowInfo,
        data_dir: str = "data"
    ) -> List[UIElement]:
        """
        Pipeline complet de détection d'éléments.
        
        Args:
            screenshot: Screenshot numpy array
            window_info: Informations sur la fenêtre
            data_dir: Répertoire de données
            
        Returns:
            Liste de UIElement détectés
        """
        elements = []
        
        try:
            # Étape 1: Proposer des régions
            if self.logger:
                self.logger.log_action({"action": "detection_started"})
            
            regions = self.region_proposer.propose_regions(screenshot, window_info)
            
            if self.logger:
                self.logger.log_action({
                    "action": "regions_proposed",
                    "count": len(regions)
                })
            
            # Étape 2: Caractériser chaque région
            characterized = []
            for i, region in enumerate(regions):
                char_elem = self.characterizer.characterize(
                    screenshot, region, window_info, data_dir
                )
                if char_elem:
                    characterized.append(char_elem)
            
            if self.logger:
                self.logger.log_action({
                    "action": "elements_characterized",
                    "count": len(characterized)
                })
            
            # Étape 3: Classifier chaque élément
            for char_elem in characterized:
                try:
                    element_type, role, confidence = self.classifier.classify(
                        char_elem, window_info
                    )
                    
                    # Créer l'UIElement
                    bbox = char_elem["bbox"]
                    label = char_elem["text_raw"] or "Élément sans texte"
                    
                    element_id = UIElement.generate_element_id(
                        app_name=window_info.app_name,
                        bbox=bbox,
                        label=label
                    )
                    
                    element = UIElement(
                        element_id=element_id,
                        type=element_type,
                        role=role,
                        bbox=bbox,
                        label=label,
                        visual=VisualData(
                            screenshot_path=char_elem["crop_path"],
                            embedding_provider="openclip_ViT-B-32",
                            embedding_vector_id=char_elem["image_embedding_path"]
                        ),
                        text=TextData(
                            raw=char_elem["text_raw"],
                            normalized=char_elem["text_normalized"],
                            embedding_provider="clip_text",
                            embedding_vector_id=char_elem["text_embedding_path"]
                        ),
                        properties=ElementProperties(
                            is_clickable=(element_type in [
                                UIElementType.BUTTON,
                                UIElementType.LINK
                            ]),
                            is_focusable=(element_type == UIElementType.TEXT_INPUT),
                            is_dangerous=("supprimer" in label.lower() or "delete" in label.lower())
                        ),
                        context=ElementContext(
                            app_name=window_info.app_name,
                            window_title=window_info.window_title,
                            workflow_hint=None
                        ),
                        tags=[],
                        confidence=confidence,
                        detection_method=char_elem["source"]
                    )
                    
                    elements.append(element)
                
                except Exception as e:
                    if self.logger:
                        self.logger.log_action({
                            "action": "element_classification_error",
                            "error": str(e)
                        })
                    # Continuer avec les autres éléments
                    continue
            
            if self.logger:
                self.logger.log_action({
                    "action": "detection_completed",
                    "elements_detected": len(elements)
                })
        
        except Exception as e:
            if self.logger:
                self.logger.log_action({
                    "action": "detection_pipeline_error",
                    "error": str(e)
                })
        
        return elements


if __name__ == "__main__":
    # Tests basiques
    print("UIElementDetector - Tests basiques")
    print("=" * 50)
    
    # Test BoundingBox
    print("\n1. Test BoundingBox:")
    bbox1 = BoundingBox(10, 10, 50, 50)
    bbox2 = BoundingBox(30, 30, 70, 70)
    print(f"   BBox1: ({bbox1.x1},{bbox1.y1},{bbox1.x2},{bbox1.y2})")
    print(f"   BBox2: ({bbox2.x1},{bbox2.y1},{bbox2.x2},{bbox2.y2})")
    print(f"   IoU: {bbox1.iou(bbox2):.2f}")
    print(f"   Area1: {bbox1.area()}")
    print(f"   Center1: {bbox1.center()}")
    
    # Test RegionProposer
    print("\n2. Test RegionProposer:")
    proposer = RegionProposer()
    print(f"   RegionProposer créé")
    print(f"   use_text_detection: {proposer.use_text_detection}")
    print(f"   use_rectangle_detection: {proposer.use_rectangle_detection}")
    
    print("\n✓ Tests basiques réussis!")
