"""
Query Strategy Router - Intelligent routing of queries to optimal search strategy

This module implements intelligent query analysis and routing to determine
whether to use FAISS (fast), ChromaDB (intelligent), or hybrid search strategies.
It preserves The Spark through optimal strategy selection based on query complexity.
"""

import re
from typing import Dict, Any, List, Optional, Tuple
import logging
from enum import Enum
from collections import defaultdict

# Configure logging
logger = logging.getLogger(__name__)


class SearchStrategy(Enum):
    """Search strategy types."""
    FAISS = "faiss"
    CHROMADB = "chromadb"
    HYBRID = "hybrid"


class QueryStrategyRouter:
    """
    Intelligent routing of queries to optimal search strategy.
    
    This router analyzes query characteristics to determine the best
    search engine for each query, optimizing for speed and intelligence.
    """
    
    def __init__(self):
        """Initialize query strategy router."""
        # Strategy patterns with confidence scores
        self.strategy_patterns = {
            'simple_keyword': {
                'patterns': [
                    r'^[\w\s]{1,20}$',  # Short single/multi word
                    r'^\w+$',  # Single word
                    r'^\w+\s+\w+$',  # Two words
                    r'^[A-Z_]+$',  # Constants/enums
                ],
                'strategy': SearchStrategy.FAISS,
                'priority': 'speed',
                'confidence_boost': 0.3
            },
            'complex_semantic': {
                'patterns': [
                    r'\b(how|why|what|when|where|who)\b',  # Question words
                    r'\b(explain|describe|understand|meaning)\b',  # Semantic requests
                    r'\b(similar|like|related|pattern)\b',  # Similarity searches
                    r'\b(between|compare|difference)\b',  # Comparison queries
                ],
                'strategy': SearchStrategy.CHROMADB,
                'priority': 'intelligence',
                'confidence_boost': 0.4
            },
            'hybrid_complex': {
                'patterns': [
                    r'find.*(?:like|similar)',  # Find with similarity
                    r'search.*pattern',  # Pattern searches
                    r'(?:show|get).*(?:recent|latest)',  # Time-based with content
                    r'\w+.*(?:but|except|not)',  # Complex boolean
                ],
                'strategy': SearchStrategy.HYBRID,
                'priority': 'balanced',
                'confidence_boost': 0.3
            }
        }
        
        # Metadata filter patterns
        self.metadata_patterns = {
            'project': r'project:\s*(\S+)',
            'type': r'type:\s*(\S+)',
            'date': r'date:\s*(\S+)',
            'tag': r'tag:\s*(\S+)',
            'after': r'after:\s*(\S+)',
            'before': r'before:\s*(\S+)',
            'sentiment': r'sentiment:\s*(\S+)',
            'complexity': r'complexity:\s*(\S+)',
        }
        
        # Complexity indicators
        self.complexity_indicators = {
            'operators': ['AND', 'OR', 'NOT', '&&', '||', '!'],
            'modifiers': ['NEAR', 'WITHIN', 'EXACT'],
            'wildcards': ['*', '?', '%'],
            'regex': [r'\\b', r'\\w', r'\\d', r'\.+', r'\*+'],
        }
        
        # Performance tracking
        self.routing_history = defaultdict(list)
        
        logger.info("QueryStrategyRouter initialized")
    
    def route_query(self, query: str, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
        """
        Route query to optimal search strategy.
        
        Args:
            query: Search query string
            context: Optional context dict with hints
            
        Returns:
            Routing decision dict with strategy, confidence, and reasoning
        """
        # Analyze query characteristics
        query_analysis = self._analyze_query(query)
        
        # Consider context hints
        if context:
            query_analysis = self._apply_context_hints(query_analysis, context)
        
        # Determine optimal strategy
        strategy_decision = self._determine_strategy(query_analysis)
        
        # Track routing decision
        self._track_routing(query, strategy_decision)
        
        return strategy_decision
    
    def _analyze_query(self, query: str) -> Dict[str, Any]:
        """
        Analyze query to determine characteristics.
        
        Returns:
            Analysis dict with detailed characteristics
        """
        query_lower = query.lower()
        words = query_lower.split()
        
        analysis = {
            'original_query': query,
            'length': len(query),
            'word_count': len(words),
            'complexity_score': 0.0,
            'semantic_indicators': [],
            'filter_requirements': {},
            'pattern_matches': [],
            'has_operators': False,
            'has_wildcards': False,
            'has_metadata': False,
        }
        
        # Check pattern matches
        for pattern_type, pattern_config in self.strategy_patterns.items():
            for pattern in pattern_config['patterns']:
                if re.search(pattern, query, re.IGNORECASE):
                    analysis['pattern_matches'].append({
                        'type': pattern_type,
                        'pattern': pattern,
                        'strategy': pattern_config['strategy'],
                        'confidence_boost': pattern_config['confidence_boost']
                    })
        
        # Detect semantic indicators
        semantic_words = [
            'how', 'why', 'what', 'when', 'where', 'who',
            'explain', 'describe', 'understand', 'meaning',
            'similar', 'like', 'related', 'pattern'
        ]
        
        for word in semantic_words:
            if word in query_lower:
                analysis['semantic_indicators'].append(word)
        
        # Check for metadata filters
        for filter_name, filter_pattern in self.metadata_patterns.items():
            match = re.search(filter_pattern, query, re.IGNORECASE)
            if match:
                analysis['filter_requirements'][filter_name] = match.group(1)
                analysis['has_metadata'] = True
        
        # Check for operators and complexity
        for operator in self.complexity_indicators['operators']:
            if operator in query or operator.lower() in query_lower:
                analysis['has_operators'] = True
                analysis['complexity_score'] += 0.2
        
        # Check for wildcards
        for wildcard in self.complexity_indicators['wildcards']:
            if wildcard in query:
                analysis['has_wildcards'] = True
                analysis['complexity_score'] += 0.1
        
        # Calculate complexity score
        analysis['complexity_score'] += self._calculate_complexity(analysis)
        
        # Add confidence and reasoning
        analysis['confidence'] = self._calculate_confidence(analysis)
        analysis['reasoning'] = self._generate_reasoning(analysis)
        
        return analysis
    
    def _calculate_complexity(self, analysis: Dict[str, Any]) -> float:
        """
        Calculate query complexity score.
        
        Returns:
            Complexity score between 0.0 and 1.0
        """
        score = 0.0
        
        # Length complexity
        if analysis['word_count'] > 10:
            score += 0.3
        elif analysis['word_count'] > 5:
            score += 0.2
        elif analysis['word_count'] > 3:
            score += 0.1
        
        # Semantic complexity
        semantic_count = len(analysis['semantic_indicators'])
        if semantic_count >= 3:
            score += 0.3
        elif semantic_count >= 2:
            score += 0.2
        elif semantic_count >= 1:
            score += 0.1
        
        # Filter complexity
        if analysis['has_metadata']:
            score += 0.2 * len(analysis['filter_requirements'])
        
        # Operator complexity
        if analysis['has_operators']:
            score += 0.2
        
        if analysis['has_wildcards']:
            score += 0.1
        
        return min(score, 1.0)
    
    def _determine_strategy(self, analysis: Dict[str, Any]) -> Dict[str, Any]:
        """
        Determine optimal strategy based on analysis.
        
        Returns:
            Strategy decision dict
        """
        # Start with base scores for each strategy
        strategy_scores = {
            SearchStrategy.FAISS: 0.5,
            SearchStrategy.CHROMADB: 0.5,
            SearchStrategy.HYBRID: 0.3
        }
        
        # Apply pattern match boosts
        for match in analysis['pattern_matches']:
            strategy = match['strategy']
            strategy_scores[strategy] += match['confidence_boost']
        
        # Adjust based on complexity
        complexity = analysis['complexity_score']
        
        if complexity < 0.3:
            # Simple query - boost FAISS
            strategy_scores[SearchStrategy.FAISS] += 0.4
            strategy_scores[SearchStrategy.CHROMADB] -= 0.2
        elif complexity > 0.7:
            # Complex query - boost ChromaDB
            strategy_scores[SearchStrategy.CHROMADB] += 0.4
            strategy_scores[SearchStrategy.FAISS] -= 0.3
        else:
            # Medium complexity - boost hybrid
            strategy_scores[SearchStrategy.HYBRID] += 0.3
        
        # Metadata filters strongly suggest ChromaDB
        if analysis['has_metadata']:
            strategy_scores[SearchStrategy.CHROMADB] += 0.5
            strategy_scores[SearchStrategy.FAISS] -= 0.3
        
        # Semantic indicators suggest ChromaDB
        if analysis['semantic_indicators']:
            boost = min(len(analysis['semantic_indicators']) * 0.15, 0.5)
            strategy_scores[SearchStrategy.CHROMADB] += boost
            strategy_scores[SearchStrategy.FAISS] -= boost * 0.5
        
        # Apply forced strategy if present
        if 'forced_strategy' in analysis:
            selected_strategy = SearchStrategy(analysis['forced_strategy'])
            confidence = 0.95
        else:
            # Select strategy with highest score
            selected_strategy = max(strategy_scores.items(), key=lambda x: x[1])[0]
            
            # Calculate confidence based on score difference
            scores = sorted(strategy_scores.values(), reverse=True)
            confidence = min(0.95, 0.5 + (scores[0] - scores[1]))
        
        return {
            'strategy': selected_strategy.value,
            'confidence': confidence,
            'reasoning': analysis['reasoning'],
            'complexity': analysis['complexity_score'],
            'analysis': analysis,
            'scores': {k.value: v for k, v in strategy_scores.items()}
        }
    
    def _calculate_confidence(self, analysis: Dict[str, Any]) -> float:
        """
        Calculate confidence in query analysis.
        
        Returns:
            Confidence score between 0.0 and 1.0
        """
        confidence = 0.5  # Base confidence
        
        # Pattern matches increase confidence
        if analysis['pattern_matches']:
            confidence += 0.1 * min(len(analysis['pattern_matches']), 3)
        
        # Clear indicators increase confidence
        if analysis['semantic_indicators']:
            confidence += 0.05 * min(len(analysis['semantic_indicators']), 4)
        
        # Metadata filters are clear signals
        if analysis['has_metadata']:
            confidence += 0.2
        
        # Very short or very long queries reduce confidence
        if analysis['word_count'] < 2 or analysis['word_count'] > 20:
            confidence -= 0.1
        
        return max(0.1, min(0.95, confidence))
    
    def _generate_reasoning(self, analysis: Dict[str, Any]) -> str:
        """
        Generate human-readable reasoning for the analysis.
        
        Returns:
            Reasoning string
        """
        reasons = []
        
        # Length-based reasoning
        if analysis['word_count'] <= 3:
            reasons.append("Simple keyword query")
        elif analysis['word_count'] > 10:
            reasons.append("Complex multi-part query")
        
        # Pattern-based reasoning
        if analysis['pattern_matches']:
            pattern_types = set(m['type'] for m in analysis['pattern_matches'])
            if 'simple_keyword' in pattern_types:
                reasons.append("Matches simple keyword pattern")
            if 'complex_semantic' in pattern_types:
                reasons.append("Contains semantic search indicators")
            if 'hybrid_complex' in pattern_types:
                reasons.append("Complex query structure detected")
        
        # Semantic reasoning
        if analysis['semantic_indicators']:
            indicators = ', '.join(analysis['semantic_indicators'][:3])
            reasons.append(f"Semantic indicators: {indicators}")
        
        # Metadata reasoning
        if analysis['has_metadata']:
            filters = ', '.join(analysis['filter_requirements'].keys())
            reasons.append(f"Metadata filters: {filters}")
        
        # Operator reasoning
        if analysis['has_operators']:
            reasons.append("Contains boolean operators")
        
        if analysis['has_wildcards']:
            reasons.append("Contains wildcard patterns")
        
        # Complexity reasoning
        if analysis['complexity_score'] < 0.3:
            reasons.append("Low complexity suitable for fast search")
        elif analysis['complexity_score'] > 0.7:
            reasons.append("High complexity requires intelligent search")
        else:
            reasons.append("Medium complexity benefits from hybrid approach")
        
        return "; ".join(reasons) if reasons else "Standard query analysis"
    
    def _apply_context_hints(self, analysis: Dict[str, Any], 
                           context: Dict[str, Any]) -> Dict[str, Any]:
        """
        Apply context hints to the analysis.
        
        Args:
            analysis: Current analysis dict
            context: Context hints dict
            
        Returns:
            Updated analysis dict
        """
        # Force strategy if specified
        if 'force_strategy' in context:
            analysis['forced_strategy'] = context['force_strategy']
            analysis['reasoning'] += f"; Forced strategy: {context['force_strategy']}"
        
        # Add context-based filters
        if 'filters' in context:
            for key, value in context['filters'].items():
                if key not in analysis['filter_requirements']:
                    analysis['filter_requirements'][key] = value
                    analysis['has_metadata'] = True
        
        # Boost complexity for certain contexts
        if context.get('require_semantic', False):
            analysis['complexity_score'] += 0.3
            analysis['reasoning'] += "; Semantic search required by context"
        
        return analysis
    
    def _track_routing(self, query: str, decision: Dict[str, Any]):
        """Track routing decision for performance analysis."""
        self.routing_history[decision['strategy']].append({
            'query': query[:50],  # Truncate for privacy
            'confidence': decision['confidence'],
            'complexity': decision['complexity'],
            'timestamp': None  # Would add timestamp in production
        })
        
        # Keep only last 100 entries per strategy
        if len(self.routing_history[decision['strategy']]) > 100:
            self.routing_history[decision['strategy']].pop(0)
    
    def get_routing_stats(self) -> Dict[str, Any]:
        """
        Get routing statistics for monitoring.
        
        Returns:
            Statistics dict
        """
        stats = {}
        
        for strategy, history in self.routing_history.items():
            if history:
                confidences = [h['confidence'] for h in history]
                complexities = [h['complexity'] for h in history]
                
                stats[strategy] = {
                    'count': len(history),
                    'avg_confidence': sum(confidences) / len(confidences),
                    'avg_complexity': sum(complexities) / len(complexities),
                    'min_confidence': min(confidences),
                    'max_confidence': max(confidences)
                }
            else:
                stats[strategy] = {'count': 0}
        
        return stats
    
    def optimize_patterns(self) -> Dict[str, Any]:
        """
        Analyze routing history to optimize patterns.
        
        Returns:
            Optimization suggestions
        """
        suggestions = {
            'pattern_adjustments': [],
            'threshold_changes': [],
            'new_patterns': []
        }
        
        # Analyze low confidence routes
        for strategy, history in self.routing_history.items():
            low_confidence = [h for h in history if h['confidence'] < 0.6]
            
            if len(low_confidence) > 5:
                suggestions['pattern_adjustments'].append({
                    'strategy': strategy,
                    'issue': 'Low confidence routes',
                    'count': len(low_confidence),
                    'recommendation': 'Review pattern matches for these queries'
                })
        
        # Check for strategy imbalance
        total_routes = sum(len(h) for h in self.routing_history.values())
        if total_routes > 50:
            for strategy, history in self.routing_history.items():
                ratio = len(history) / total_routes
                
                if ratio < 0.1:
                    suggestions['threshold_changes'].append({
                        'strategy': strategy,
                        'current_ratio': ratio,
                        'recommendation': f'{strategy} underutilized, consider lowering thresholds'
                    })
                elif ratio > 0.7:
                    suggestions['threshold_changes'].append({
                        'strategy': strategy,
                        'current_ratio': ratio,
                        'recommendation': f'{strategy} overutilized, consider raising thresholds'
                    })
        
        return suggestions