"""Find similar code patterns in the repository.

Uses AST-based similarity to find code patterns that are similar
to a given snippet, helping identify how similar issues were handled.
"""

from __future__ import annotations

import ast
from dataclasses import dataclass
from typing import TYPE_CHECKING, Dict, List, Optional, Set

if TYPE_CHECKING:
    from autodebug.context.repo_indexer import RepoIndexer, SymbolInfo


@dataclass
class ASTPattern:
    """A pattern extracted from AST for matching."""

    node_types: List[str]  # Sequence of AST node types
    structure_hash: str  # Hash of the structure
    depth: int  # Maximum nesting depth
    control_flow: List[str]  # Control flow patterns (if, for, while, try)
    calls: List[str]  # Function calls made
    operators: List[str]  # Operators used
    exceptions: List[str]  # Exception types handled

    def __str__(self) -> str:
        cf = ", ".join(self.control_flow) if self.control_flow else "none"
        return f"Pattern(depth={self.depth}, control_flow=[{cf}], calls={len(self.calls)})"


@dataclass
class SimilarCode:
    """A piece of code similar to the query."""

    file_path: str
    start_line: int
    end_line: int
    code: str
    similarity_score: float  # 0.0 to 1.0
    pattern: ASTPattern
    symbol_name: Optional[str] = None
    match_reason: str = ""

    def __str__(self) -> str:
        return f"Similar({self.file_path}:{self.start_line}, score={self.similarity_score:.2f})"


class SimilarityFinder:
    """Find similar code patterns in a repository.

    Uses AST-based similarity metrics to find code that is
    structurally similar to a given snippet.
    """

    def __init__(self, indexer: RepoIndexer):
        """Initialize the similarity finder.

        Args:
            indexer: Repository indexer to use
        """
        self.indexer = indexer
        self._pattern_cache: Dict[str, ASTPattern] = {}

    def find_similar(
        self,
        code: str,
        file_path: Optional[str] = None,
        limit: int = 10,
        min_similarity: float = 0.3,
        exclude_files: Optional[Set[str]] = None,
    ) -> List[SimilarCode]:
        """Find code similar to the given snippet.

        Args:
            code: Source code to find similar patterns for
            file_path: File path of the source code (to exclude from results)
            limit: Maximum results to return
            min_similarity: Minimum similarity score
            exclude_files: Files to exclude from results

        Returns:
            List of similar code snippets
        """
        exclude_files = exclude_files or set()
        if file_path:
            exclude_files.add(file_path)

        # Extract pattern from query code
        query_pattern = self._extract_pattern(code)
        if not query_pattern:
            return []

        results: List[SimilarCode] = []

        # Ensure indexed
        if not self.indexer._indexed:
            self.indexer.index()

        # Search through all functions
        for symbol in self.indexer.symbol_table.symbols.values():
            if symbol.file_path in exclude_files:
                continue

            # Get code for this symbol
            symbol_code = self._get_code(symbol)
            if not symbol_code:
                continue

            # Extract and compare patterns
            symbol_pattern = self._extract_pattern(symbol_code)
            if not symbol_pattern:
                continue

            similarity = self._compute_similarity(query_pattern, symbol_pattern)

            if similarity >= min_similarity:
                match_reason = self._get_match_reason(query_pattern, symbol_pattern)
                results.append(SimilarCode(
                    file_path=symbol.file_path,
                    start_line=symbol.start_line,
                    end_line=symbol.end_line,
                    code=symbol_code,
                    similarity_score=similarity,
                    pattern=symbol_pattern,
                    symbol_name=symbol.name,
                    match_reason=match_reason,
                ))

        # Sort by similarity and return top results
        results.sort(key=lambda x: x.similarity_score, reverse=True)
        return results[:limit]

    def find_similar_exception_handlers(
        self,
        exception_type: str,
        limit: int = 5,
    ) -> List[SimilarCode]:
        """Find code that handles a specific exception type.

        Args:
            exception_type: Type of exception to find handlers for
            limit: Maximum results

        Returns:
            List of code snippets that handle this exception
        """
        results: List[SimilarCode] = []

        if not self.indexer._indexed:
            self.indexer.index()

        for symbol in self.indexer.symbol_table.symbols.values():
            code = self._get_code(symbol)
            if not code:
                continue

            pattern = self._extract_pattern(code)
            if not pattern:
                continue

            # Check if this code handles the exception
            if exception_type in pattern.exceptions or "Exception" in pattern.exceptions:
                results.append(SimilarCode(
                    file_path=symbol.file_path,
                    start_line=symbol.start_line,
                    end_line=symbol.end_line,
                    code=code,
                    similarity_score=1.0 if exception_type in pattern.exceptions else 0.5,
                    pattern=pattern,
                    symbol_name=symbol.name,
                    match_reason=f"Handles {exception_type}",
                ))

        results.sort(key=lambda x: x.similarity_score, reverse=True)
        return results[:limit]

    def find_similar_patterns_to_error(
        self,
        error_code: str,
        exception_type: str,
        limit: int = 5,
    ) -> List[SimilarCode]:
        """Find code with similar patterns to error-prone code.

        Args:
            error_code: Code that caused an error
            exception_type: Type of exception raised
            limit: Maximum results

        Returns:
            Similar code that may have the same issue or solution
        """
        # First, find structurally similar code
        similar = self.find_similar(error_code, limit=limit * 2)

        # Then filter/boost based on exception handling
        for result in similar:
            if exception_type in result.pattern.exceptions:
                result.similarity_score = min(result.similarity_score + 0.2, 1.0)
                result.match_reason += f"; handles {exception_type}"

        similar.sort(key=lambda x: x.similarity_score, reverse=True)
        return similar[:limit]

    def _extract_pattern(self, code: str) -> Optional[ASTPattern]:
        """Extract an AST pattern from code.

        Args:
            code: Python source code

        Returns:
            ASTPattern or None if code can't be parsed
        """
        # Check cache
        code_hash = str(hash(code))
        if code_hash in self._pattern_cache:
            return self._pattern_cache[code_hash]

        try:
            tree = ast.parse(code)
        except SyntaxError:
            return None

        extractor = _PatternExtractor()
        extractor.visit(tree)

        pattern = ASTPattern(
            node_types=extractor.node_types,
            structure_hash=self._compute_structure_hash(tree),
            depth=extractor.max_depth,
            control_flow=extractor.control_flow,
            calls=extractor.calls,
            operators=extractor.operators,
            exceptions=extractor.exceptions,
        )

        self._pattern_cache[code_hash] = pattern
        return pattern

    def _compute_structure_hash(self, tree: ast.AST) -> str:
        """Compute a hash of the AST structure.

        Args:
            tree: AST tree

        Returns:
            Hash string
        """
        # Build a simplified structure representation
        structure = []

        class StructureVisitor(ast.NodeVisitor):
            def generic_visit(self, node: ast.AST) -> None:
                structure.append(type(node).__name__)
                super().generic_visit(node)

        StructureVisitor().visit(tree)
        return str(hash(tuple(structure[:50])))  # Limit to first 50 nodes

    def _compute_similarity(
        self,
        pattern1: ASTPattern,
        pattern2: ASTPattern,
    ) -> float:
        """Compute similarity between two patterns.

        Args:
            pattern1: First pattern
            pattern2: Second pattern

        Returns:
            Similarity score 0.0 to 1.0
        """
        scores = []

        # Control flow similarity
        cf_sim = self._jaccard_similarity(
            set(pattern1.control_flow),
            set(pattern2.control_flow),
        )
        scores.append(cf_sim * 0.25)

        # Call similarity
        call_sim = self._jaccard_similarity(
            set(pattern1.calls),
            set(pattern2.calls),
        )
        scores.append(call_sim * 0.25)

        # Operator similarity
        op_sim = self._jaccard_similarity(
            set(pattern1.operators),
            set(pattern2.operators),
        )
        scores.append(op_sim * 0.15)

        # Exception handling similarity
        exc_sim = self._jaccard_similarity(
            set(pattern1.exceptions),
            set(pattern2.exceptions),
        )
        scores.append(exc_sim * 0.15)

        # Depth similarity
        depth_diff = abs(pattern1.depth - pattern2.depth)
        depth_sim = 1.0 / (1.0 + depth_diff)
        scores.append(depth_sim * 0.1)

        # Node type similarity (first few node types)
        node_sim = self._sequence_similarity(
            pattern1.node_types[:20],
            pattern2.node_types[:20],
        )
        scores.append(node_sim * 0.1)

        return sum(scores)

    def _jaccard_similarity(self, set1: Set[str], set2: Set[str]) -> float:
        """Compute Jaccard similarity between two sets.

        Args:
            set1: First set
            set2: Second set

        Returns:
            Jaccard similarity 0.0 to 1.0
        """
        if not set1 and not set2:
            return 1.0
        if not set1 or not set2:
            return 0.0

        intersection = len(set1 & set2)
        union = len(set1 | set2)
        return intersection / union if union > 0 else 0.0

    def _sequence_similarity(self, seq1: List[str], seq2: List[str]) -> float:
        """Compute similarity between two sequences.

        Uses simple positional matching for speed.

        Args:
            seq1: First sequence
            seq2: Second sequence

        Returns:
            Similarity score 0.0 to 1.0
        """
        if not seq1 and not seq2:
            return 1.0
        if not seq1 or not seq2:
            return 0.0

        min_len = min(len(seq1), len(seq2))
        max_len = max(len(seq1), len(seq2))

        matches = sum(1 for i in range(min_len) if seq1[i] == seq2[i])

        return matches / max_len if max_len > 0 else 0.0

    def _get_match_reason(
        self,
        query: ASTPattern,
        match: ASTPattern,
    ) -> str:
        """Get a human-readable reason for the match.

        Args:
            query: Query pattern
            match: Matched pattern

        Returns:
            Match reason string
        """
        reasons = []

        # Check control flow
        cf_overlap = set(query.control_flow) & set(match.control_flow)
        if cf_overlap:
            reasons.append(f"same control flow: {', '.join(cf_overlap)}")

        # Check calls
        call_overlap = set(query.calls) & set(match.calls)
        if call_overlap:
            limited = list(call_overlap)[:3]
            reasons.append(f"calls: {', '.join(limited)}")

        # Check exceptions
        exc_overlap = set(query.exceptions) & set(match.exceptions)
        if exc_overlap:
            reasons.append(f"handles: {', '.join(exc_overlap)}")

        return "; ".join(reasons) if reasons else "similar structure"

    def _get_code(self, symbol: SymbolInfo) -> Optional[str]:
        """Get source code for a symbol."""
        try:
            full_path = self.indexer.root_path / symbol.file_path
            if not full_path.exists():
                return None

            lines = full_path.read_text(encoding="utf-8").split("\n")
            start = symbol.start_line - 1
            end = symbol.end_line
            return "\n".join(lines[start:end])
        except (OSError, UnicodeDecodeError):
            return None


class _PatternExtractor(ast.NodeVisitor):
    """Extract pattern information from AST."""

    def __init__(self):
        self.node_types: List[str] = []
        self.control_flow: List[str] = []
        self.calls: List[str] = []
        self.operators: List[str] = []
        self.exceptions: List[str] = []
        self.max_depth = 0
        self._current_depth = 0

    def generic_visit(self, node: ast.AST) -> None:
        """Visit any node."""
        self.node_types.append(type(node).__name__)
        self._current_depth += 1
        self.max_depth = max(self.max_depth, self._current_depth)
        super().generic_visit(node)
        self._current_depth -= 1

    def visit_If(self, node: ast.If) -> None:
        """Track if statements."""
        self.control_flow.append("if")
        self.generic_visit(node)

    def visit_For(self, node: ast.For) -> None:
        """Track for loops."""
        self.control_flow.append("for")
        self.generic_visit(node)

    def visit_While(self, node: ast.While) -> None:
        """Track while loops."""
        self.control_flow.append("while")
        self.generic_visit(node)

    def visit_Try(self, node: ast.Try) -> None:
        """Track try blocks."""
        self.control_flow.append("try")
        self.generic_visit(node)

    def visit_With(self, node: ast.With) -> None:
        """Track with statements."""
        self.control_flow.append("with")
        self.generic_visit(node)

    def visit_AsyncFor(self, node: ast.AsyncFor) -> None:
        """Track async for loops."""
        self.control_flow.append("async_for")
        self.generic_visit(node)

    def visit_AsyncWith(self, node: ast.AsyncWith) -> None:
        """Track async with statements."""
        self.control_flow.append("async_with")
        self.generic_visit(node)

    def visit_Call(self, node: ast.Call) -> None:
        """Track function calls."""
        call_name = self._get_call_name(node)
        if call_name:
            self.calls.append(call_name)
        self.generic_visit(node)

    def visit_ExceptHandler(self, node: ast.ExceptHandler) -> None:
        """Track exception handlers."""
        if node.type:
            if isinstance(node.type, ast.Name):
                self.exceptions.append(node.type.id)
            elif isinstance(node.type, ast.Tuple):
                for elt in node.type.elts:
                    if isinstance(elt, ast.Name):
                        self.exceptions.append(elt.id)
        else:
            self.exceptions.append("bare_except")
        self.generic_visit(node)

    def visit_BinOp(self, node: ast.BinOp) -> None:
        """Track binary operators."""
        self.operators.append(type(node.op).__name__)
        self.generic_visit(node)

    def visit_Compare(self, node: ast.Compare) -> None:
        """Track comparison operators."""
        for op in node.ops:
            self.operators.append(type(op).__name__)
        self.generic_visit(node)

    def visit_BoolOp(self, node: ast.BoolOp) -> None:
        """Track boolean operators."""
        self.operators.append(type(node.op).__name__)
        self.generic_visit(node)

    def visit_UnaryOp(self, node: ast.UnaryOp) -> None:
        """Track unary operators."""
        self.operators.append(type(node.op).__name__)
        self.generic_visit(node)

    def _get_call_name(self, node: ast.Call) -> Optional[str]:
        """Get the name of a function call."""
        if isinstance(node.func, ast.Name):
            return node.func.id
        elif isinstance(node.func, ast.Attribute):
            return node.func.attr
        return None
