"""
Query Engine Module

This module provides comprehensive SPARQL query execution and optimization
for triple store operations, including query planning, caching, and performance
monitoring.

Key Features:
    - SPARQL query execution and optimization
    - Query planning and caching
    - Result processing and formatting
    - Performance monitoring and profiling
    - Query validation
    - Multi-store query support
    - Query history tracking

Main Classes:
    - QueryEngine: Main query execution and optimization coordinator
    - QueryResult: SPARQL query result representation dataclass
    - QueryPlan: Query execution plan representation dataclass

Example Usage:
    >>> from semantica.triple_store import QueryEngine
    >>> engine = QueryEngine(enable_caching=True, enable_optimization=True)
    >>> result = engine.execute_query(sparql_query, store_adapter)
    >>> plan = engine.plan_query(sparql_query)
    >>> stats = engine.get_query_statistics()

Author: Semantica Contributors
License: MIT
"""

from typing import Any, Dict, List, Optional
from dataclasses import dataclass, field
from datetime import datetime
import time

from ..utils.exceptions import ValidationError, ProcessingError
from ..utils.logging import get_logger
from ..utils.progress_tracker import get_progress_tracker


@dataclass
class QueryResult:
    """SPARQL query result."""
    bindings: List[Dict[str, Any]]
    variables: List[str]
    execution_time: float = 0.0
    metadata: Dict[str, Any] = field(default_factory=dict)


@dataclass
class QueryPlan:
    """Query execution plan."""
    query: str
    optimized_query: str
    estimated_cost: float = 0.0
    execution_steps: List[str] = field(default_factory=list)
    metadata: Dict[str, Any] = field(default_factory=dict)


class QueryEngine:
    """
    SPARQL query execution and optimization engine.
    
    • SPARQL query execution and optimization
    • Query planning and caching
    • Result processing and formatting
    • Performance monitoring and profiling
    • Error handling and validation
    • Multi-store query support
    """
    
    def __init__(self, config: Optional[Dict[str, Any]] = None, **kwargs):
        """
        Initialize query engine.
        
        Args:
            config: Configuration dictionary
            **kwargs: Additional configuration options:
                - enable_caching: Enable query caching (default: True)
                - cache_size: Cache size limit
                - enable_optimization: Enable query optimization (default: True)
        """
        self.logger = get_logger("query_engine")
        self.config = config or {}
        self.config.update(kwargs)
        self.progress_tracker = get_progress_tracker()
        
        self.enable_caching = self.config.get("enable_caching", True)
        self.enable_optimization = self.config.get("enable_optimization", True)
        self.cache_size = self.config.get("cache_size", 1000)
        
        self.query_cache: Dict[str, QueryResult] = {}
        self.query_history: List[Dict[str, Any]] = []
    
    def execute_query(
        self,
        query: str,
        store_adapter: Any,
        **options
    ) -> QueryResult:
        """
        Execute SPARQL query.
        
        Args:
            query: SPARQL query string
            store_adapter: Triple store adapter instance
            **options: Additional options
        
        Returns:
            Query result
        """
        tracking_id = self.progress_tracker.start_tracking(
            module="triple_store",
            submodule="QueryEngine",
            message="Executing SPARQL query"
        )
        
        try:
            start_time = time.time()
            
            # Validate query
            self.progress_tracker.update_tracking(tracking_id, message="Validating query...")
            if not self._validate_query(query):
                self.progress_tracker.stop_tracking(tracking_id, status="failed", message="Invalid SPARQL query")
                raise ValidationError("Invalid SPARQL query")
            
            # Check cache
            if self.enable_caching:
                self.progress_tracker.update_tracking(tracking_id, message="Checking cache...")
                cache_key = self._get_cache_key(query)
                if cache_key in self.query_cache:
                    self.logger.debug("Returning cached query result")
                    cached_result = self.query_cache[cache_key]
                    cached_result.metadata["cached"] = True
                    self.progress_tracker.stop_tracking(tracking_id, status="completed", message="Returned cached result")
                    return cached_result
            
            # Optimize query
            if self.enable_optimization:
                self.progress_tracker.update_tracking(tracking_id, message="Optimizing query...")
                optimized_query = self.optimize_query(query, **options)
            else:
                optimized_query = query
            
            # Execute query
            self.progress_tracker.update_tracking(tracking_id, message="Executing query on store...")
            if hasattr(store_adapter, "execute_sparql"):
                result_data = store_adapter.execute_sparql(optimized_query, **options)
            else:
                raise ProcessingError("Store adapter does not support SPARQL execution")
            
            execution_time = time.time() - start_time
            
            result = QueryResult(
                bindings=result_data.get("bindings", []),
                variables=result_data.get("variables", []),
                execution_time=execution_time,
                metadata={
                    **result_data.get("metadata", {}),
                    "optimized": optimized_query != query,
                    "cached": False
                }
            )
            
            # Cache result
            if self.enable_caching:
                self.progress_tracker.update_tracking(tracking_id, message="Caching result...")
                self._cache_result(query, result)
            
            # Record history
            self.query_history.append({
                "query": query,
                "execution_time": execution_time,
                "result_count": len(result.bindings),
                "timestamp": datetime.now().isoformat()
            })
            
            self.progress_tracker.stop_tracking(tracking_id, status="completed",
                                              message=f"Query executed: {len(result.bindings)} results in {execution_time:.2f}s")
            return result
            
        except Exception as e:
            execution_time = time.time() - start_time if 'start_time' in locals() else 0.0
            self.logger.error(f"Query execution failed: {e}")
            self.progress_tracker.stop_tracking(tracking_id, status="failed", message=str(e))
            raise ProcessingError(f"Query execution failed: {e}")
    
    def optimize_query(
        self,
        query: str,
        **options
    ) -> str:
        """
        Optimize SPARQL query.
        
        Args:
            query: Original query
            **options: Optimization options
        
        Returns:
            Optimized query
        """
        optimized = query.strip()
        
        # Remove unnecessary whitespace
        optimized = " ".join(optimized.split())
        
        # Add LIMIT if SELECT query doesn't have one
        if "SELECT" in optimized.upper() and "LIMIT" not in optimized.upper():
            if options.get("add_limit", True):
                default_limit = options.get("default_limit", 1000)
                optimized += f" LIMIT {default_limit}"
        
        # Basic query rewriting
        # (More sophisticated optimization would require query parser)
        
        return optimized
    
    def plan_query(
        self,
        query: str,
        **options
    ) -> QueryPlan:
        """
        Create query execution plan.
        
        Args:
            query: SPARQL query
            **options: Planning options
        
        Returns:
            Query execution plan
        """
        optimized_query = self.optimize_query(query, **options) if self.enable_optimization else query
        
        # Estimate cost (simplified)
        estimated_cost = self._estimate_query_cost(query)
        
        # Identify execution steps
        execution_steps = self._identify_execution_steps(query)
        
        return QueryPlan(
            query=query,
            optimized_query=optimized_query,
            estimated_cost=estimated_cost,
            execution_steps=execution_steps,
            metadata={
                "optimization_enabled": self.enable_optimization
            }
        )
    
    def _validate_query(self, query: str) -> bool:
        """Validate SPARQL query syntax (basic)."""
        if not query or not query.strip():
            return False
        
        query_upper = query.upper()
        
        # Check for valid SPARQL keywords
        valid_keywords = ["SELECT", "ASK", "CONSTRUCT", "DESCRIBE", "INSERT", "DELETE", "WHERE"]
        if not any(keyword in query_upper for keyword in valid_keywords):
            return False
        
        return True
    
    def _estimate_query_cost(self, query: str) -> float:
        """Estimate query execution cost."""
        # Simple heuristic based on query complexity
        cost = 1.0
        
        # COUNT queries are more expensive
        if "COUNT" in query.upper():
            cost *= 2.0
        
        # Multiple joins increase cost
        join_count = query.upper().count("JOIN") + query.count(".")
        cost *= (1.0 + join_count * 0.1)
        
        # DISTINCT increases cost
        if "DISTINCT" in query.upper():
            cost *= 1.5
        
        return cost
    
    def _identify_execution_steps(self, query: str) -> List[str]:
        """Identify query execution steps."""
        steps = []
        
        query_upper = query.upper()
        
        if "SELECT" in query_upper:
            steps.append("SELECT projection")
        if "WHERE" in query_upper:
            steps.append("Pattern matching")
        if "FILTER" in query_upper:
            steps.append("Filtering")
        if "ORDER BY" in query_upper:
            steps.append("Sorting")
        if "LIMIT" in query_upper or "OFFSET" in query_upper:
            steps.append("Pagination")
        
        return steps
    
    def _get_cache_key(self, query: str) -> str:
        """Generate cache key for query."""
        import hashlib
        normalized = " ".join(query.split())
        return hashlib.md5(normalized.encode()).hexdigest()
    
    def _cache_result(self, query: str, result: QueryResult) -> None:
        """Cache query result."""
        if len(self.query_cache) >= self.cache_size:
            # Remove oldest entry
            oldest_key = next(iter(self.query_cache))
            del self.query_cache[oldest_key]
        
        cache_key = self._get_cache_key(query)
        self.query_cache[cache_key] = result
    
    def clear_cache(self) -> None:
        """Clear query cache."""
        self.query_cache.clear()
    
    def get_query_statistics(self) -> Dict[str, Any]:
        """Get query execution statistics."""
        if not self.query_history:
            return {
                "total_queries": 0,
                "average_execution_time": 0.0,
                "total_execution_time": 0.0
            }
        
        execution_times = [q["execution_time"] for q in self.query_history]
        
        return {
            "total_queries": len(self.query_history),
            "average_execution_time": sum(execution_times) / len(execution_times),
            "total_execution_time": sum(execution_times),
            "min_execution_time": min(execution_times),
            "max_execution_time": max(execution_times),
            "cache_size": len(self.query_cache),
            "cache_hit_rate": 0.0  # Would need to track hits/misses
        }
