"""Pattern matching for finding similar fixes.

This module implements similarity matching between new failures and
stored patterns. Uses multiple algorithms inspired by TraceSim research:
- Exception type exact matching
- Stack trace similarity (Levenshtein, LCS, TF-IDF)
- Code pattern similarity
- Message pattern matching

Reference: https://arxiv.org/abs/2009.12590
"""

from __future__ import annotations

import logging
import re
from collections import Counter
from dataclasses import dataclass
from difflib import SequenceMatcher
from typing import Any, Dict, List, Optional, Tuple

from autodebug.patterns.extractor import (
    extract_exception_signature,
)
from autodebug.patterns.store import (
    ExceptionSignature,
    FixPattern,
    PatternMatch,
    PatternStore,
    StackFrame,
    get_pattern_store,
)

logger = logging.getLogger(__name__)


@dataclass
class MatchConfig:
    """Configuration for pattern matching."""

    # Weights for different similarity components
    exception_type_weight: float = 0.3
    stack_similarity_weight: float = 0.35
    message_similarity_weight: float = 0.2
    code_pattern_weight: float = 0.15

    # Thresholds
    min_similarity: float = 0.5  # Minimum overall similarity to return
    min_stack_similarity: float = 0.3  # Minimum stack similarity

    # Limits
    max_candidates: int = 100  # Max patterns to evaluate
    max_results: int = 10  # Max matches to return

    # Algorithm selection
    stack_algorithm: str = "combined"  # "levenshtein", "lcs", "tfidf" (cosine), "combined"


class PatternMatcher:
    """Finds patterns similar to a given failure."""

    def __init__(
        self,
        store: Optional[PatternStore] = None,
        config: Optional[MatchConfig] = None,
    ):
        """Initialize the matcher.

        Args:
            store: Pattern store to search (uses default if None)
            config: Matching configuration
        """
        self.store = store or get_pattern_store()
        self.config = config or MatchConfig()

    def find_matches(
        self,
        exception_data: Dict[str, Any],
        code_context: Optional[str] = None,
        language: str = "python",
    ) -> List[PatternMatch]:
        """Find patterns matching a failure.

        Args:
            exception_data: Exception info (type, message, stack)
            code_context: Optional code around the failure
            language: Programming language

        Returns:
            List of matching patterns sorted by recommendation score
        """
        # Extract signature from the failure
        target_sig = extract_exception_signature(exception_data)

        # Get candidate patterns
        candidates = self._get_candidates(target_sig)

        if not candidates:
            logger.debug("No candidate patterns found")
            return []

        # Score each candidate
        matches = []
        for pattern in candidates:
            score, details = self._calculate_similarity(
                target_sig, pattern, code_context, language
            )

            if score >= self.config.min_similarity:
                # Try to instantiate the fix template
                suggested_fix = self._instantiate_fix(
                    pattern, exception_data, code_context
                )

                matches.append(PatternMatch(
                    pattern=pattern,
                    similarity_score=score,
                    match_details=details,
                    suggested_fix=suggested_fix,
                ))

        # Sort by recommendation score (combines similarity and success rate)
        matches.sort(key=lambda m: m.recommendation_score, reverse=True)

        return matches[:self.config.max_results]

    def _get_candidates(
        self,
        target_sig: ExceptionSignature,
    ) -> List[FixPattern]:
        """Get candidate patterns for matching."""
        candidates = []

        # First, exact exception type matches
        exact_matches = self.store.find_by_exception_type(
            target_sig.exception_type,
            limit=self.config.max_candidates // 2,
        )
        candidates.extend(exact_matches)

        # Also get patterns with similar stack signatures
        stack_matches = self.store.find_by_stack_signature(
            target_sig.stack_signature,
            limit=self.config.max_candidates // 4,
        )
        candidates.extend(stack_matches)

        # Get some high-success patterns as fallback
        if len(candidates) < self.config.max_candidates // 2:
            top_patterns = self.store.search(
                min_success_rate=0.7,
                min_uses=3,
                limit=self.config.max_candidates // 4,
            )
            candidates.extend(top_patterns)

        # Deduplicate by ID
        seen = set()
        unique = []
        for p in candidates:
            if p.id not in seen:
                seen.add(p.id)
                unique.append(p)

        return unique[:self.config.max_candidates]

    def _calculate_similarity(
        self,
        target: ExceptionSignature,
        pattern: FixPattern,
        code_context: Optional[str],
        language: str,
    ) -> Tuple[float, Dict[str, float]]:
        """Calculate similarity between target and pattern."""
        details = {}

        # Exception type similarity
        exc_sim = self._exception_type_similarity(
            target.exception_type,
            pattern.exception_signature.exception_type,
        )
        details["exception_type"] = exc_sim

        # Stack trace similarity
        stack_sim = self._stack_similarity(
            target.top_frames,
            pattern.exception_signature.top_frames,
        )
        details["stack_trace"] = stack_sim

        # Message similarity
        msg_sim = self._message_similarity(
            target.message_pattern,
            pattern.exception_signature.message_pattern,
        )
        details["message"] = msg_sim

        # Code pattern similarity (if context provided)
        code_sim = 0.0
        if code_context and pattern.code_pattern:
            code_sim = self._code_similarity(
                code_context,
                pattern.code_pattern.code_signature,
                language,
            )
        details["code_pattern"] = code_sim

        # Weighted combination
        total = (
            exc_sim * self.config.exception_type_weight +
            stack_sim * self.config.stack_similarity_weight +
            msg_sim * self.config.message_similarity_weight +
            code_sim * self.config.code_pattern_weight
        )

        return total, details

    def _exception_type_similarity(
        self,
        type1: str,
        type2: str,
    ) -> float:
        """Calculate similarity between exception types."""
        if type1 == type2:
            return 1.0

        # Normalize types
        t1 = type1.lower().replace("error", "").replace("exception", "")
        t2 = type2.lower().replace("error", "").replace("exception", "")

        if t1 == t2:
            return 0.9

        # Check for partial matches
        if t1 in t2 or t2 in t1:
            return 0.7

        # Check common error families
        families = {
            "type": ["typeerror", "typemismatch", "invalidtype"],
            "value": ["valueerror", "invalidvalue", "illegalargument"],
            "key": ["keyerror", "keynotfound", "nosuchkey"],
            "index": ["indexerror", "outofbounds", "indexoutofrange"],
            "attribute": ["attributeerror", "nosuchattribute", "undefined"],
            "null": ["nullpointer", "nonetype", "null", "nil"],
            "connection": ["connectionerror", "networkerror", "socketerror"],
            "timeout": ["timeouterror", "timeout", "timedout"],
        }

        t1_lower = type1.lower()
        t2_lower = type2.lower()

        for family, members in families.items():
            in_family_1 = any(m in t1_lower for m in members)
            in_family_2 = any(m in t2_lower for m in members)
            if in_family_1 and in_family_2:
                return 0.6

        return 0.0

    def _stack_similarity(
        self,
        frames1: List[StackFrame],
        frames2: List[StackFrame],
    ) -> float:
        """Calculate stack trace similarity."""
        if not frames1 or not frames2:
            return 0.0

        algorithm = self.config.stack_algorithm

        if algorithm == "levenshtein":
            return self._stack_levenshtein(frames1, frames2)
        elif algorithm == "lcs":
            return self._stack_lcs(frames1, frames2)
        elif algorithm == "tfidf":
            return self._stack_cosine_similarity(frames1, frames2)
        else:  # combined
            lev = self._stack_levenshtein(frames1, frames2)
            lcs = self._stack_lcs(frames1, frames2)
            cosine = self._stack_cosine_similarity(frames1, frames2)
            return (lev * 0.4 + lcs * 0.3 + cosine * 0.3)

    def _stack_levenshtein(
        self,
        frames1: List[StackFrame],
        frames2: List[StackFrame],
    ) -> float:
        """Levenshtein-based stack similarity."""
        sigs1 = [f.to_signature() for f in frames1]
        sigs2 = [f.to_signature() for f in frames2]

        # Use SequenceMatcher for edit distance ratio
        matcher = SequenceMatcher(None, sigs1, sigs2)
        return matcher.ratio()

    def _stack_lcs(
        self,
        frames1: List[StackFrame],
        frames2: List[StackFrame],
    ) -> float:
        """Longest common subsequence based similarity."""
        sigs1 = [f.to_signature() for f in frames1]
        sigs2 = [f.to_signature() for f in frames2]

        # DP for LCS
        m, n = len(sigs1), len(sigs2)
        dp = [[0] * (n + 1) for _ in range(m + 1)]

        for i in range(1, m + 1):
            for j in range(1, n + 1):
                if sigs1[i - 1] == sigs2[j - 1]:
                    dp[i][j] = dp[i - 1][j - 1] + 1
                else:
                    dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

        lcs_len = dp[m][n]
        return (2 * lcs_len) / (m + n) if (m + n) > 0 else 0.0

    def _stack_cosine_similarity(
        self,
        frames1: List[StackFrame],
        frames2: List[StackFrame],
    ) -> float:
        """Token-frequency cosine similarity for stack frames."""
        # Convert frames to tokens
        tokens1 = []
        tokens2 = []

        for f in frames1:
            tokens1.extend([
                f.function,
                f.to_signature(),
                f.file.split('/')[-1] if f.file else "unknown",
            ])

        for f in frames2:
            tokens2.extend([
                f.function,
                f.to_signature(),
                f.file.split('/')[-1] if f.file else "unknown",
            ])

        # Calculate TF
        tf1 = Counter(tokens1)
        tf2 = Counter(tokens2)

        # Normalize
        norm1 = sum(v * v for v in tf1.values()) ** 0.5
        norm2 = sum(v * v for v in tf2.values()) ** 0.5

        if norm1 == 0 or norm2 == 0:
            return 0.0

        # Cosine similarity
        common = set(tf1.keys()) & set(tf2.keys())
        dot_product = sum(tf1[k] * tf2[k] for k in common)

        return dot_product / (norm1 * norm2)

    def _message_similarity(
        self,
        msg1: str,
        msg2: str,
    ) -> float:
        """Calculate message pattern similarity."""
        if not msg1 or not msg2:
            return 0.0

        if msg1 == msg2:
            return 1.0

        # Use SequenceMatcher for fuzzy matching
        return SequenceMatcher(None, msg1, msg2).ratio()

    def _code_similarity(
        self,
        code1: str,
        code2: str,
        language: str,
    ) -> float:
        """Calculate code pattern similarity."""
        if not code1 or not code2:
            return 0.0

        # Tokenize code
        tokens1 = self._tokenize_code(code1, language)
        tokens2 = self._tokenize_code(code2, language)

        if not tokens1 or not tokens2:
            return 0.0

        # Jaccard similarity of tokens
        set1 = set(tokens1)
        set2 = set(tokens2)

        intersection = len(set1 & set2)
        union = len(set1 | set2)

        return intersection / union if union > 0 else 0.0

    def _tokenize_code(self, code: str, language: str) -> List[str]:
        """Tokenize code for similarity comparison."""
        # Remove comments
        if language == "python":
            code = re.sub(r'#.*$', '', code, flags=re.MULTILINE)
            code = re.sub(r'""".*?"""', '', code, flags=re.DOTALL)
            code = re.sub(r"'''.*?'''", '', code, flags=re.DOTALL)
        elif language in ("javascript", "typescript", "java", "go"):
            code = re.sub(r'//.*$', '', code, flags=re.MULTILINE)
            code = re.sub(r'/\*.*?\*/', '', code, flags=re.DOTALL)

        # Extract tokens
        tokens = re.findall(r'\b\w+\b', code)

        # Filter out common stop words and very short tokens
        stop_words = {
            'a', 'an', 'the', 'is', 'are', 'was', 'were', 'be', 'been',
            'to', 'of', 'in', 'for', 'on', 'with', 'as', 'at', 'by',
        }

        return [t for t in tokens if len(t) > 1 and t.lower() not in stop_words]

    def _instantiate_fix(
        self,
        pattern: FixPattern,
        exception_data: Dict[str, Any],
        code_context: Optional[str],
    ) -> Optional[str]:
        """Try to instantiate the fix template for this specific case."""
        if not pattern.fix_template:
            return None

        template = pattern.fix_template

        # Try to fill in template variables
        context = {
            "exception_type": exception_data.get("exc_type", "Exception"),
            "message": exception_data.get("message", ""),
        }

        # Extract variable names from code context
        if code_context:
            # Find variable assignments
            var_match = re.search(r'(\w+)\s*=', code_context)
            if var_match:
                context["variable"] = var_match.group(1)

            # Find function calls
            call_match = re.search(r'(\w+)\s*\(', code_context)
            if call_match:
                context["function"] = call_match.group(1)

        try:
            # Safe format with default values
            result = template
            for key in re.findall(r'\{(\w+)\}', template):
                if key in context:
                    result = result.replace(f'{{{key}}}', str(context[key]))
                else:
                    result = result.replace(f'{{{key}}}', f'<{key}>')
            return result
        except Exception:
            return None


def find_similar_patterns(
    exception_data: Dict[str, Any],
    code_context: Optional[str] = None,
    language: str = "python",
    min_similarity: float = 0.5,
    max_results: int = 5,
) -> List[PatternMatch]:
    """Convenience function to find matching patterns.

    Args:
        exception_data: Exception info (type, message, stack)
        code_context: Optional code around the failure
        language: Programming language
        min_similarity: Minimum similarity threshold
        max_results: Maximum matches to return

    Returns:
        List of matching patterns
    """
    config = MatchConfig(
        min_similarity=min_similarity,
        max_results=max_results,
    )
    matcher = PatternMatcher(config=config)
    return matcher.find_matches(exception_data, code_context, language)


def format_match_report(matches: List[PatternMatch]) -> str:
    """Format matches as a human-readable report."""
    if not matches:
        return "No matching patterns found."

    lines = [
        "=" * 60,
        "SIMILAR PATTERNS FOUND",
        "=" * 60,
        "",
    ]

    for i, match in enumerate(matches, 1):
        pattern = match.pattern

        lines.append(f"{i}. {pattern.fix_type.upper()}")
        lines.append(f"   Similarity: {match.similarity_score:.0%}")
        lines.append(f"   Success Rate: {pattern.success_rate:.0%} "
                     f"({pattern.success_count}/{pattern.success_count + pattern.failure_count})")
        lines.append(f"   Recommendation Score: {match.recommendation_score:.0%}")
        lines.append(f"   Description: {pattern.fix_description}")

        # Show match details
        lines.append("   Match Breakdown:")
        for component, score in match.match_details.items():
            lines.append(f"     - {component}: {score:.0%}")

        if match.suggested_fix:
            lines.append("   Suggested Fix:")
            for fix_line in match.suggested_fix.splitlines()[:5]:
                lines.append(f"     {fix_line}")
            if len(match.suggested_fix.splitlines()) > 5:
                lines.append("     ...")

        lines.append("")

    # Summary
    best = matches[0]
    lines.append("-" * 60)
    lines.append(f"Best match: {best.pattern.fix_type}")
    lines.append(f"This pattern has fixed {best.pattern.success_count} similar bugs "
                 f"with {best.pattern.success_rate:.0%} success rate.")
    lines.append("")

    return "\n".join(lines)
