"""Match local exceptions to global patterns.

Finds relevant patterns from the global cross-organization
database to help with fix generation.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, List, Optional

if TYPE_CHECKING:
    from autodebug.learning.cross_org_aggregator import (
        AnonymousPattern,
        CrossOrgAggregator,
    )


@dataclass
class PatternMatch:
    """A matched pattern with confidence score.

    Attributes:
        pattern: The matched anonymous pattern
        score: Match quality score (0-1)
        confidence: Confidence based on global acceptance rate
        match_reasons: List of reasons why this matched
    """

    pattern: AnonymousPattern
    score: float
    confidence: float
    match_reasons: List[str]

    @property
    def combined_score(self) -> float:
        """Combined score for ranking."""
        return self.score * self.confidence

    def to_dict(self) -> dict:
        """Convert to dictionary."""
        return {
            "pattern": self.pattern.to_dict(),
            "score": self.score,
            "confidence": self.confidence,
            "match_reasons": self.match_reasons,
            "combined_score": self.combined_score,
        }


class PatternMatcher:
    """Matches local exceptions to global patterns.

    Uses multiple signals to find the best matching patterns:
    - Exception type match
    - Message pattern similarity
    - Stack trace similarity
    - Framework/library match
    """

    # Minimum score to consider a match
    MIN_MATCH_SCORE = 0.5

    # Weights for different match signals
    WEIGHTS = {
        "exception_type": 0.3,
        "message_pattern": 0.25,
        "stack_similarity": 0.2,
        "framework_match": 0.15,
        "org_count_bonus": 0.1,
    }

    def __init__(self, aggregator: CrossOrgAggregator):
        """Initialize pattern matcher.

        Args:
            aggregator: Cross-organization aggregator for pattern access
        """
        self.aggregator = aggregator

    def find_matching_patterns(
        self,
        exception_type: str,
        exception_message: str,
        stack_trace: List[str],
        framework: Optional[str] = None,
        max_results: int = 10,
    ) -> List[PatternMatch]:
        """Find global patterns matching this exception.

        Args:
            exception_type: Type of the exception
            exception_message: Exception message
            stack_trace: Stack trace lines
            framework: Optional framework context
            max_results: Maximum patterns to return

        Returns:
            List of matched patterns, sorted by combined score
        """
        # Anonymize local exception
        message_pattern = self.aggregator._extract_message_pattern(exception_message)
        stack_pattern = self.aggregator._anonymize_stack(stack_trace)

        # Detect framework if not provided
        if not framework:
            framework = self.aggregator._detect_framework(stack_trace)

        # Get candidate patterns from database
        candidates = self.aggregator.get_patterns_for_exception(
            exception_type=exception_type,
            message_pattern=message_pattern,
            framework=framework,
        )

        # Score each candidate
        matches = []
        for pattern in candidates:
            score, reasons = self._calculate_match_score(
                pattern, exception_type, message_pattern, stack_pattern, framework
            )

            if score >= self.MIN_MATCH_SCORE:
                matches.append(
                    PatternMatch(
                        pattern=pattern,
                        score=score,
                        confidence=pattern.global_acceptance_rate,
                        match_reasons=reasons,
                    )
                )

        # Sort by combined score
        matches.sort(key=lambda m: m.combined_score, reverse=True)

        return matches[:max_results]

    def _calculate_match_score(
        self,
        pattern: AnonymousPattern,
        exception_type: str,
        message_pattern: str,
        stack_pattern: List[str],
        framework: Optional[str],
    ) -> tuple[float, List[str]]:
        """Calculate how well a pattern matches.

        Args:
            pattern: Pattern to score
            exception_type: Target exception type
            message_pattern: Target message pattern
            stack_pattern: Target stack pattern
            framework: Target framework

        Returns:
            Tuple of (score, list of match reasons)
        """
        score = 0.0
        reasons = []

        # Exception type match (required)
        if pattern.exception_type == exception_type:
            score += self.WEIGHTS["exception_type"]
            reasons.append(f"Exception type: {exception_type}")
        else:
            # No match on exception type means no match at all
            return 0.0, []

        # Message pattern similarity
        msg_score = self._message_similarity(pattern.message_pattern, message_pattern)
        if msg_score > 0.8:
            score += self.WEIGHTS["message_pattern"]
            reasons.append("Strong message pattern match")
        elif msg_score > 0.5:
            score += self.WEIGHTS["message_pattern"] * 0.5
            reasons.append("Partial message pattern match")

        # Stack pattern similarity
        stack_score = self._stack_similarity(pattern.stack_pattern, stack_pattern)
        if stack_score > 0.6:
            score += self.WEIGHTS["stack_similarity"]
            reasons.append(f"Stack similarity: {stack_score:.0%}")
        elif stack_score > 0.3:
            score += self.WEIGHTS["stack_similarity"] * 0.5
            reasons.append(f"Partial stack match: {stack_score:.0%}")

        # Framework match
        if framework and pattern.framework:
            if pattern.framework == framework:
                score += self.WEIGHTS["framework_match"]
                reasons.append(f"Framework: {framework}")

        # Org count bonus (more orgs = more confidence)
        if pattern.org_count >= 10:
            score += self.WEIGHTS["org_count_bonus"]
            reasons.append(f"Seen by {pattern.org_count}+ organizations")
        elif pattern.org_count >= 5:
            score += self.WEIGHTS["org_count_bonus"] * 0.5
            reasons.append(f"Seen by {pattern.org_count} organizations")

        return min(score, 1.0), reasons

    def _message_similarity(self, p1: str, p2: str) -> float:
        """Calculate message pattern similarity.

        Args:
            p1: First message pattern
            p2: Second message pattern

        Returns:
            Similarity score (0-1)
        """
        if p1 == p2:
            return 1.0

        # Token-based similarity (Jaccard)
        tokens1 = set(p1.split())
        tokens2 = set(p2.split())

        if not tokens1 or not tokens2:
            return 1.0 if p1 == p2 else 0.0

        intersection = len(tokens1 & tokens2)
        union = len(tokens1 | tokens2)

        if union == 0:
            return 0.0

        return intersection / union

    def _stack_similarity(self, s1: List[str], s2: List[str]) -> float:
        """Calculate stack pattern similarity.

        Args:
            s1: First stack pattern
            s2: Second stack pattern

        Returns:
            Similarity score (0-1)
        """
        if not s1 or not s2:
            return 0.0

        # Positional matching (important for stack traces)
        matches = 0
        for i, (f1, f2) in enumerate(zip(s1, s2)):
            if f1 == f2:
                # Exact match at same position
                matches += 1
            elif self._functions_similar(f1, f2):
                # Similar function names
                matches += 0.5

        return matches / max(len(s1), len(s2))

    def _functions_similar(self, f1: str, f2: str) -> bool:
        """Check if two function names are similar.

        Args:
            f1: First function name
            f2: Second function name

        Returns:
            True if similar
        """
        # Both are generalized patterns
        if f1 == f2:
            return True

        # Check if they share a common pattern prefix
        patterns = [
            "get_MODEL_by_FIELD",
            "create_MODEL",
            "update_MODEL",
            "delete_MODEL",
            "test_CASE",
            "handle_EVENT",
            "process_ITEM",
            "validate_ITEM",
        ]

        for pattern in patterns:
            if pattern in f1 and pattern in f2:
                return True

        return False

    def get_best_strategy(
        self,
        exception_type: str,
        exception_message: str,
        stack_trace: List[str],
    ) -> Optional[str]:
        """Get the best strategy recommendation.

        Args:
            exception_type: Type of exception
            exception_message: Exception message
            stack_trace: Stack trace

        Returns:
            Best strategy name or None
        """
        matches = self.find_matching_patterns(
            exception_type=exception_type,
            exception_message=exception_message,
            stack_trace=stack_trace,
            max_results=1,
        )

        if matches:
            return matches[0].pattern.fix_strategy

        return None

    def get_strategy_rankings(
        self,
        exception_type: str,
        exception_message: str,
        stack_trace: List[str],
    ) -> List[tuple[str, float]]:
        """Get ranked strategies with scores.

        Args:
            exception_type: Type of exception
            exception_message: Exception message
            stack_trace: Stack trace

        Returns:
            List of (strategy, score) tuples
        """
        matches = self.find_matching_patterns(
            exception_type=exception_type,
            exception_message=exception_message,
            stack_trace=stack_trace,
            max_results=10,
        )

        # Aggregate scores by strategy
        strategy_scores: dict[str, float] = {}
        for match in matches:
            strategy = match.pattern.fix_strategy
            if strategy not in strategy_scores:
                strategy_scores[strategy] = match.combined_score
            else:
                # Keep best score for each strategy
                strategy_scores[strategy] = max(
                    strategy_scores[strategy], match.combined_score
                )

        # Sort by score
        return sorted(strategy_scores.items(), key=lambda x: x[1], reverse=True)


class PatternRecommendationEngine:
    """High-level interface for pattern-based recommendations.

    Combines pattern matching with additional context to provide
    actionable recommendations.
    """

    def __init__(self, matcher: PatternMatcher):
        """Initialize recommendation engine.

        Args:
            matcher: Pattern matcher instance
        """
        self.matcher = matcher

    def get_recommendations(
        self,
        exception_type: str,
        exception_message: str,
        stack_trace: List[str],
        code_context: Optional[str] = None,
    ) -> dict:
        """Get comprehensive recommendations for an exception.

        Args:
            exception_type: Type of exception
            exception_message: Exception message
            stack_trace: Stack trace
            code_context: Optional surrounding code

        Returns:
            Dict with recommendations
        """
        matches = self.matcher.find_matching_patterns(
            exception_type=exception_type,
            exception_message=exception_message,
            stack_trace=stack_trace,
        )

        strategies = self.matcher.get_strategy_rankings(
            exception_type=exception_type,
            exception_message=exception_message,
            stack_trace=stack_trace,
        )

        return {
            "top_matches": [m.to_dict() for m in matches[:3]],
            "recommended_strategies": [
                {"strategy": s, "score": score} for s, score in strategies[:5]
            ],
            "match_count": len(matches),
            "has_strong_match": any(m.score > 0.8 for m in matches),
            "confidence": matches[0].confidence if matches else 0.0,
        }

    def should_use_global_patterns(
        self,
        exception_type: str,
        exception_message: str,
        local_history_count: int,
    ) -> bool:
        """Determine if global patterns should be used.

        Args:
            exception_type: Type of exception
            exception_message: Exception message
            local_history_count: Number of local examples

        Returns:
            True if global patterns should be used
        """
        # Use global patterns if:
        # 1. Few local examples
        if local_history_count < 5:
            return True

        # 2. Exception type is common across orgs
        message_pattern = self.matcher.aggregator._extract_message_pattern(
            exception_message
        )
        patterns = self.matcher.aggregator.get_patterns_for_exception(
            exception_type=exception_type,
            message_pattern=message_pattern,
        )

        # High org count suggests common issue
        if patterns and patterns[0].org_count >= 10:
            return True

        return False
