"""
High-performance CSS selector engine for Sprint 4 optimizations
"""
from typing import List, Dict, Any, Optional
import re
import time
import threading
from dataclasses import dataclass
from enum import Enum
from .performance import LRUCache, get_performance_monitor

# CSS Selector Types for Optimization
class SelectorType(Enum):
    TAG_ONLY = "tag_only"           # div
    CLASS_ONLY = "class_only"       # .classname
    ID_ONLY = "id_only"             # #idname
    ATTRIBUTE_ONLY = "attr_only"    # [attr=value]
    DESCENDANT = "descendant"       # div p
    CHILD = "child"                 # div > p
    ADJACENT_SIBLING = "adjacent"   # div + p
    GENERAL_SIBLING = "general"     # div ~ p
    COMPLEX = "complex"             # Complex multi-part selectors


@dataclass
class SelectorPlan:
    """Optimized query execution plan for CSS selectors."""
    selector_type: SelectorType
    primary_filter: str
    secondary_filters: List[Dict[str, Any]]
    optimization_hints: Dict[str, Any]
    estimated_selectivity: float  # 0.0 (very selective) to 1.0 (matches everything)


class SelectorCompiler:
    """Compiles CSS selectors into optimized execution plans."""
    
    def __init__(self):
        self._compilation_cache = LRUCache(max_size=1000)
        self._selectivity_stats = {}
        self._lock = threading.RLock()
    
    def compile_selector(self, selector: str) -> SelectorPlan:
        """Compile CSS selector into optimized execution plan."""
        with self._lock:
            # Check cache first
            cached_plan = self._compilation_cache.get(selector)
            if cached_plan:
                return cached_plan
            
            # Analyze selector structure
            plan = self._analyze_and_optimize(selector)
            
            # Cache the plan
            self._compilation_cache.put(selector, plan)
            
            return plan
    
    def _analyze_and_optimize(self, selector: str) -> SelectorPlan:
        """Analyze selector and create optimized execution plan."""
        selector = selector.strip()
        
        # Simple optimizations for common patterns
        if self._is_simple_tag(selector):
            return SelectorPlan(
                selector_type=SelectorType.TAG_ONLY,
                primary_filter=selector,
                secondary_filters=[],
                optimization_hints={"use_tag_index": True},
                estimated_selectivity=self._estimate_tag_selectivity(selector)
            )
        
        elif self._is_simple_class(selector):
            class_name = selector[1:]  # Remove leading dot
            return SelectorPlan(
                selector_type=SelectorType.CLASS_ONLY,
                primary_filter=class_name,
                secondary_filters=[],
                optimization_hints={"use_class_index": True},
                estimated_selectivity=self._estimate_class_selectivity(class_name)
            )
        
        elif self._is_simple_id(selector):
            id_name = selector[1:]  # Remove leading hash
            return SelectorPlan(
                selector_type=SelectorType.ID_ONLY,
                primary_filter=id_name,
                secondary_filters=[],
                optimization_hints={"use_id_index": True, "single_result": True},
                estimated_selectivity=0.001  # IDs are highly selective
            )
        
        elif ' > ' in selector:
            return self._compile_child_selector(selector)
        
        elif ' + ' in selector:
            return self._compile_adjacent_sibling_selector(selector)
        
        elif ' ~ ' in selector:
            return self._compile_general_sibling_selector(selector)
        
        elif ' ' in selector:
            return self._compile_descendant_selector(selector)
        
        else:
            return self._compile_complex_selector(selector)
    
    def _is_simple_tag(self, selector: str) -> bool:
        """Check if selector is just a tag name."""
        return re.match(r'^[a-zA-Z][a-zA-Z0-9]*$', selector) is not None
    
    def _is_simple_class(self, selector: str) -> bool:
        """Check if selector is just a class name."""
        return re.match(r'^\.[a-zA-Z_][a-zA-Z0-9_-]*$', selector) is not None
    
    def _is_simple_id(self, selector: str) -> bool:
        """Check if selector is just an ID."""
        return re.match(r'^#[a-zA-Z_][a-zA-Z0-9_-]*$', selector) is not None
    
    def _compile_descendant_selector(self, selector: str) -> SelectorPlan:
        """Compile descendant selector (space-separated)."""
        parts = [part.strip() for part in selector.split() if part.strip()]
        
        # Use most selective part as primary filter
        primary_part = self._find_most_selective_part(parts)
        secondary_parts = [part for part in parts if part != primary_part]
        
        return SelectorPlan(
            selector_type=SelectorType.DESCENDANT,
            primary_filter=primary_part,
            secondary_filters=[{"part": part, "relation": "descendant"} for part in secondary_parts],
            optimization_hints={"reverse_search": len(parts) > 2},
            estimated_selectivity=self._estimate_combined_selectivity(parts)
        )
    
    def _compile_child_selector(self, selector: str) -> SelectorPlan:
        """Compile direct child selector."""
        parts = [part.strip() for part in selector.split(' > ') if part.strip()]
        
        return SelectorPlan(
            selector_type=SelectorType.CHILD,
            primary_filter=parts[-1],  # Start from child
            secondary_filters=[{"part": part, "relation": "parent"} for part in parts[:-1]],
            optimization_hints={"bottom_up_search": True},
            estimated_selectivity=self._estimate_combined_selectivity(parts) * 0.1  # More selective
        )
    
    def _compile_adjacent_sibling_selector(self, selector: str) -> SelectorPlan:
        """Compile adjacent sibling selector."""
        parts = [part.strip() for part in selector.split(' + ') if part.strip()]
        
        return SelectorPlan(
            selector_type=SelectorType.ADJACENT_SIBLING,
            primary_filter=parts[-1],
            secondary_filters=[{"part": part, "relation": "prev_sibling"} for part in parts[:-1]],
            optimization_hints={"sibling_search": True},
            estimated_selectivity=self._estimate_combined_selectivity(parts) * 0.05
        )
    
    def _compile_general_sibling_selector(self, selector: str) -> SelectorPlan:
        """Compile general sibling selector."""
        parts = [part.strip() for part in selector.split(' ~ ') if part.strip()]
        
        return SelectorPlan(
            selector_type=SelectorType.GENERAL_SIBLING,
            primary_filter=parts[-1],
            secondary_filters=[{"part": part, "relation": "prev_siblings"} for part in parts[:-1]],
            optimization_hints={"sibling_search": True},
            estimated_selectivity=self._estimate_combined_selectivity(parts) * 0.2
        )
    
    def _compile_complex_selector(self, selector: str) -> SelectorPlan:
        """Compile complex selector with multiple parts."""
        return SelectorPlan(
            selector_type=SelectorType.COMPLEX,
            primary_filter=selector,
            secondary_filters=[],
            optimization_hints={"fallback_to_python": True},
            estimated_selectivity=0.5  # Conservative estimate
        )
    
    def _find_most_selective_part(self, parts: List[str]) -> str:
        """Find the most selective part to use as primary filter."""
        selectivity_scores = {}
        
        for part in parts:
            if part.startswith('#'):
                selectivity_scores[part] = 0.001  # ID is most selective
            elif part.startswith('.'):
                selectivity_scores[part] = self._estimate_class_selectivity(part[1:])
            else:
                selectivity_scores[part] = self._estimate_tag_selectivity(part)
        
        # Return part with lowest selectivity score (most selective)
        return min(selectivity_scores.keys(), key=lambda k: selectivity_scores[k])
    
    def _estimate_tag_selectivity(self, tag: str) -> float:
        """Estimate selectivity for tag selectors."""
        # Common tags are less selective
        common_tags = {'div': 0.3, 'span': 0.2, 'p': 0.15, 'a': 0.1, 'img': 0.05}
        return common_tags.get(tag.lower(), 0.08)
    
    def _estimate_class_selectivity(self, class_name: str) -> float:
        """Estimate selectivity for class selectors."""
        # Use historical data if available
        return self._selectivity_stats.get(f"class_{class_name}", 0.05)
    
    def _estimate_combined_selectivity(self, parts: List[str]) -> float:
        """Estimate selectivity for combined selectors."""
        individual_selectivities = []
        
        for part in parts:
            if part.startswith('#'):
                individual_selectivities.append(0.001)
            elif part.startswith('.'):
                individual_selectivities.append(self._estimate_class_selectivity(part[1:]))
            else:
                individual_selectivities.append(self._estimate_tag_selectivity(part))
        
        # Combined selectivity is roughly the product
        combined = 1.0
        for sel in individual_selectivities:
            combined *= sel
        
        return min(combined, 0.8)  # Cap at 0.8
    
    def update_selectivity_stats(self, selector: str, actual_matches: int, total_nodes: int):
        """Update selectivity statistics based on actual query results."""
        if total_nodes > 0:
            actual_selectivity = actual_matches / total_nodes
            self._selectivity_stats[selector] = actual_selectivity


class HighPerformanceCSS:
    """High-performance CSS selector engine with advanced optimizations."""
    
    def __init__(self):
        self.compiler = SelectorCompiler()
        self.monitor = get_performance_monitor()
        self._query_cache = LRUCache(max_size=500)
        self._statistics = {
            "cache_hits": 0,
            "cache_misses": 0,
            "optimized_queries": 0,
            "fallback_queries": 0
        }
    
    def select_optimized(self, root_tag, selector: str) -> List:
        """Optimized CSS selector with performance improvements."""
        start_time = time.perf_counter()
        
        try:
            # Check query cache first
            cache_key = (id(root_tag._tree), selector)
            cached_result = self._query_cache.get(cache_key)
            if cached_result is not None:
                self._statistics["cache_hits"] += 1
                return cached_result
            
            self._statistics["cache_misses"] += 1
            
            # Compile selector into execution plan
            plan = self.compiler.compile_selector(selector)
            
            # Execute optimized query based on plan
            if plan.selector_type == SelectorType.TAG_ONLY:
                results = self._execute_tag_only_query(root_tag, plan)
                self._statistics["optimized_queries"] += 1
            
            elif plan.selector_type == SelectorType.CLASS_ONLY:
                results = self._execute_class_only_query(root_tag, plan)
                self._statistics["optimized_queries"] += 1
            
            elif plan.selector_type == SelectorType.ID_ONLY:
                results = self._execute_id_only_query(root_tag, plan)
                self._statistics["optimized_queries"] += 1
            
            elif plan.selector_type == SelectorType.DESCENDANT:
                results = self._execute_descendant_query(root_tag, plan)
                self._statistics["optimized_queries"] += 1
            
            else:
                # Fallback to original CSS selector for complex cases
                results = self._execute_fallback_query(root_tag, selector)
                self._statistics["fallback_queries"] += 1
            
            # Cache results
            self._query_cache.put(cache_key, results)
            
            # Update selectivity statistics
            if hasattr(root_tag, 'descendants'):
                total_nodes = len(list(root_tag.descendants)) + 1
                self.compiler.update_selectivity_stats(selector, len(results), total_nodes)
            
            return results
            
        finally:
            self.monitor.end_timer("css_selector_optimized", start_time)
    
    def _execute_tag_only_query(self, root_tag, plan: SelectorPlan) -> List:
        """Execute optimized tag-only query."""
        tag_name = plan.primary_filter
        results = []
        
        # Optimized tag search - avoid Python iteration where possible
        if hasattr(root_tag, '_node') and root_tag._node:
            # Use C++ find_all if available
            try:
                node_results = root_tag._node.find_all(tag_name)
                for node in node_results:
                    from .core import Tag
                    results.append(Tag._create_from_tree_node(node, root_tag._tree))
                return results
            except AttributeError:
                pass
        
        # Fallback to descendants iteration
        if hasattr(root_tag, 'descendants'):
            for descendant in root_tag.descendants:
                if descendant.name == tag_name:
                    results.append(descendant)
        else:
            # Handle CutySoup document root
            try:
                for descendant in root_tag.find_all(tag_name):
                    results.append(descendant)
            except (AttributeError, TypeError):
                pass
        
        return results
    
    def _execute_class_only_query(self, root_tag, plan: SelectorPlan) -> List:
        """Execute optimized class-only query."""
        class_name = plan.primary_filter
        results = []
        
        # Optimized class search
        if hasattr(root_tag, 'descendants'):
            for descendant in root_tag.descendants:
                tag_classes = descendant.get('class', '').split()
                if class_name in tag_classes:
                    results.append(descendant)
        else:
            # Handle CutySoup document root
            try:
                for descendant in root_tag.find_all(attrs={'class': class_name}):
                    results.append(descendant)
            except (AttributeError, TypeError):
                pass
        
        return results
    
    def _execute_id_only_query(self, root_tag, plan: SelectorPlan) -> List:
        """Execute optimized ID-only query."""
        id_name = plan.primary_filter
        
        # ID should be unique, so we can stop at first match
        if hasattr(root_tag, 'descendants'):
            for descendant in root_tag.descendants:
                if descendant.get('id') == id_name:
                    return [descendant]
        else:
            # Handle CutySoup document root
            try:
                result = root_tag.find(id=id_name)
                return [result] if result else []
            except (AttributeError, TypeError):
                pass
        
        return []
    
    def _execute_descendant_query(self, root_tag, plan: SelectorPlan) -> List:
        """Execute optimized descendant query."""
        # Start with most selective part
        primary_results = []
        
        if plan.primary_filter.startswith('#'):
            primary_results = self._execute_id_only_query(
                root_tag, 
                SelectorPlan(SelectorType.ID_ONLY, plan.primary_filter[1:], [], {}, 0.001)
            )
        elif plan.primary_filter.startswith('.'):
            primary_results = self._execute_class_only_query(
                root_tag,
                SelectorPlan(SelectorType.CLASS_ONLY, plan.primary_filter[1:], [], {}, 0.05)
            )
        else:
            primary_results = self._execute_tag_only_query(
                root_tag,
                SelectorPlan(SelectorType.TAG_ONLY, plan.primary_filter, [], {}, 0.1)
            )
        
        # Filter by secondary constraints
        final_results = []
        for candidate in primary_results:
            if self._matches_secondary_filters(candidate, plan.secondary_filters):
                final_results.append(candidate)
        
        return final_results
    
    def _matches_secondary_filters(self, tag, secondary_filters: List[Dict[str, Any]]) -> bool:
        """Check if tag matches secondary filter constraints."""
        for filter_info in secondary_filters:
            part = filter_info["part"]
            relation = filter_info["relation"]
            
            if relation == "descendant":
                if not self._has_descendant_matching(tag, part):
                    return False
            elif relation == "parent":
                if not self._has_parent_matching(tag, part):
                    return False
        
        return True
    
    def _has_descendant_matching(self, tag, part: str) -> bool:
        """Check if tag has a descendant matching the part."""
        try:
            for ancestor in tag.parents:
                if self._matches_part(ancestor, part):
                    return True
        except (AttributeError, TypeError):
            pass
        return False
    
    def _has_parent_matching(self, tag, part: str) -> bool:
        """Check if tag has a parent matching the part."""
        try:
            parent = tag.parent
            return parent and self._matches_part(parent, part)
        except (AttributeError, TypeError):
            return False
    
    def _matches_part(self, tag, part: str) -> bool:
        """Check if tag matches a selector part."""
        if part.startswith('#'):
            return tag.get('id') == part[1:]
        elif part.startswith('.'):
            class_attr = tag.get('class', '')
            if isinstance(class_attr, list):
                tag_classes = class_attr
            else:
                tag_classes = class_attr.split() if class_attr else []
            return part[1:] in tag_classes
        else:
            return tag.name == part
    
    def _execute_fallback_query(self, root_tag, selector: str) -> List:
        """Execute query using original CSS selector as fallback."""
        from .css_selectors import select_tags
        return select_tags(root_tag, selector)
    
    def get_performance_stats(self) -> Dict[str, Any]:
        """Get performance statistics."""
        total_queries = self._statistics["cache_hits"] + self._statistics["cache_misses"]
        
        stats = self._statistics.copy()
        if total_queries > 0:
            stats["cache_hit_rate"] = self._statistics["cache_hits"] / total_queries
            stats["optimization_rate"] = self._statistics["optimized_queries"] / total_queries
        
        stats["query_cache_stats"] = self._query_cache.get_stats()
        
        return stats
    
    def clear_caches(self):
        """Clear all caches."""
        self._query_cache.clear()
        self.compiler._compilation_cache.clear()


# Global high-performance CSS engine
_global_css_engine: Optional[HighPerformanceCSS] = None


def get_css_engine() -> HighPerformanceCSS:
    """Get global high-performance CSS engine."""
    global _global_css_engine
    if _global_css_engine is None:
        _global_css_engine = HighPerformanceCSS()
    return _global_css_engine


def select_fast(root_tag, selector: str) -> List:
    """Fast CSS selector with performance optimizations."""
    engine = get_css_engine()
    return engine.select_optimized(root_tag, selector)


def select_one_fast(root_tag, selector: str):
    """Fast CSS selector returning first match."""
    results = select_fast(root_tag, selector)
    return results[0] if results else None


# Performance monitoring decorator
def css_performance_monitor(func):
    """Decorator to monitor CSS selector performance."""
    def wrapper(*args, **kwargs):
        monitor = get_performance_monitor()
        start_time = monitor.start_timer("css_selector")
        try:
            result = func(*args, **kwargs)
            return result
        finally:
            monitor.end_timer("css_selector", start_time)
    
    return wrapper