"""Build relevant context for debugging a failure.

Given a failure location, builds context including callers,
callees, types, similar code, and related tests.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import TYPE_CHECKING, Any, Dict, List, Optional

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


class ContextType(str, Enum):
    """Type of context item."""

    FAILING_CODE = "failing_code"
    CALLER = "caller"
    CALLEE = "callee"
    TYPE_DEFINITION = "type_definition"
    SIMILAR_CODE = "similar_code"
    TEST = "test"
    IMPORT = "import"
    RELATED = "related"


@dataclass
class ContextItem:
    """A single piece of context."""

    context_type: ContextType
    file_path: str
    start_line: int
    end_line: int
    code: str
    relevance_score: float  # 0.0 to 1.0
    description: str
    symbol_name: Optional[str] = None
    metadata: Dict[str, Any] = field(default_factory=dict)

    def __str__(self) -> str:
        return f"[{self.context_type.value}] {self.file_path}:{self.start_line} ({self.relevance_score:.2f})"

    @property
    def lines_count(self) -> int:
        """Get number of lines of code."""
        return self.end_line - self.start_line + 1


@dataclass
class ContextResult:
    """Result of context building."""

    items: List[ContextItem] = field(default_factory=list)
    total_lines: int = 0
    truncated: bool = False

    def add(self, item: ContextItem) -> None:
        """Add a context item."""
        self.items.append(item)
        self.total_lines += item.lines_count

    def get_by_type(self, context_type: ContextType) -> List[ContextItem]:
        """Get items of a specific type."""
        return [item for item in self.items if item.context_type == context_type]

    def format_for_llm(self, max_lines: int = 500) -> str:
        """Format context for LLM prompt.

        Args:
            max_lines: Maximum lines to include

        Returns:
            Formatted context string
        """
        lines = []
        current_lines = 0

        # Sort by relevance
        sorted_items = sorted(self.items, key=lambda x: x.relevance_score, reverse=True)

        for item in sorted_items:
            if current_lines + item.lines_count > max_lines:
                self.truncated = True
                continue

            lines.append(f"## {item.context_type.value.replace('_', ' ').title()}")
            lines.append(f"### {item.description}")
            lines.append(f"File: {item.file_path}:{item.start_line}-{item.end_line}")
            lines.append("```python")
            lines.append(item.code)
            lines.append("```")
            lines.append("")

            current_lines += item.lines_count

        return "\n".join(lines)


class ContextBuilder:
    """Build relevant context for debugging failures.

    Given a failure location, finds and ranks relevant context
    including callers, callees, types, and similar code.
    """

    def __init__(
        self,
        indexer: RepoIndexer,
        max_callers: int = 3,
        max_callees: int = 3,
        max_similar: int = 2,
        max_context_lines: int = 500,
    ):
        """Initialize the context builder.

        Args:
            indexer: Repository indexer to use
            max_callers: Maximum caller functions to include
            max_callees: Maximum callee functions to include
            max_similar: Maximum similar code snippets to include
            max_context_lines: Maximum total lines of context
        """
        self.indexer = indexer
        self.max_callers = max_callers
        self.max_callees = max_callees
        self.max_similar = max_similar
        self.max_context_lines = max_context_lines

        from autodebug.context.similarity_finder import SimilarityFinder as SF
        self._similarity_finder: SimilarityFinder = SF(indexer)

    def build(
        self,
        file_path: str,
        line_number: int,
        exception_type: Optional[str] = None,
        exception_message: Optional[str] = None,
        include_callers: bool = True,
        include_callees: bool = True,
        include_types: bool = True,
        include_similar: bool = True,
        include_tests: bool = True,
    ) -> ContextResult:
        """Build context for a failure location.

        Args:
            file_path: Path to the failing file
            line_number: Line number of failure
            exception_type: Type of exception
            exception_message: Exception message
            include_callers: Include caller functions
            include_callees: Include called functions
            include_types: Include type definitions
            include_similar: Include similar code
            include_tests: Include related tests

        Returns:
            ContextResult with relevant context
        """
        result = ContextResult()

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

        # Get the failing symbol
        failing_symbol = self.indexer.get_symbol_at(file_path, line_number)
        if failing_symbol:
            # Add failing code with highest relevance
            failing_code = self._get_code(failing_symbol)
            if failing_code:
                result.add(ContextItem(
                    context_type=ContextType.FAILING_CODE,
                    file_path=failing_symbol.file_path,
                    start_line=failing_symbol.start_line,
                    end_line=failing_symbol.end_line,
                    code=failing_code,
                    relevance_score=1.0,
                    description=f"Failing function: {failing_symbol.name}",
                    symbol_name=failing_symbol.name,
                ))

            # Add callers
            if include_callers:
                self._add_callers(result, failing_symbol)

            # Add callees
            if include_callees:
                self._add_callees(result, failing_symbol)

        # Add type definitions
        if include_types and exception_message:
            self._add_types(result, file_path, exception_message)

        # Add similar code patterns
        if include_similar and failing_symbol:
            self._add_similar(result, failing_symbol, exception_type)

        # Add related tests
        if include_tests:
            self._add_tests(result, file_path)

        # Sort by relevance and truncate
        result.items.sort(key=lambda x: x.relevance_score, reverse=True)
        self._truncate_if_needed(result)

        return result

    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")
            # Adjust for 1-based line numbers
            start = symbol.start_line - 1
            end = symbol.end_line
            return "\n".join(lines[start:end])
        except (OSError, UnicodeDecodeError):
            return None

    def _add_callers(self, result: ContextResult, symbol: SymbolInfo) -> None:
        """Add caller functions to context."""
        callers = self.indexer.get_callers(symbol.name)

        for i, caller in enumerate(callers[: self.max_callers]):
            code = self._get_code(caller)
            if code:
                # Relevance decreases with each caller
                relevance = 0.8 - (i * 0.1)
                result.add(ContextItem(
                    context_type=ContextType.CALLER,
                    file_path=caller.file_path,
                    start_line=caller.start_line,
                    end_line=caller.end_line,
                    code=code,
                    relevance_score=relevance,
                    description=f"Calls {symbol.name}",
                    symbol_name=caller.name,
                ))

    def _add_callees(self, result: ContextResult, symbol: SymbolInfo) -> None:
        """Add called functions to context."""
        callees = self.indexer.get_callees(symbol.name)

        for i, callee in enumerate(callees[: self.max_callees]):
            code = self._get_code(callee)
            if code:
                # Relevance decreases with each callee
                relevance = 0.75 - (i * 0.1)
                result.add(ContextItem(
                    context_type=ContextType.CALLEE,
                    file_path=callee.file_path,
                    start_line=callee.start_line,
                    end_line=callee.end_line,
                    code=code,
                    relevance_score=relevance,
                    description=f"Called by {symbol.name}",
                    symbol_name=callee.name,
                ))

    def _add_types(
        self,
        result: ContextResult,
        file_path: str,
        exception_message: str,
    ) -> None:
        """Add type definitions mentioned in the exception."""
        # Extract potential type names from exception message
        import re

        # Look for class-like names (CamelCase)
        potential_types = re.findall(r"\b([A-Z][a-zA-Z0-9]+)\b", exception_message)

        for type_name in potential_types:
            type_def = self.indexer.get_type_definition(type_name)
            if type_def and type_def.file_path != file_path:
                code = self._get_code(type_def)
                if code:
                    result.add(ContextItem(
                        context_type=ContextType.TYPE_DEFINITION,
                        file_path=type_def.file_path,
                        start_line=type_def.start_line,
                        end_line=type_def.end_line,
                        code=code,
                        relevance_score=0.6,
                        description=f"Type: {type_name}",
                        symbol_name=type_name,
                    ))

    def _add_similar(
        self,
        result: ContextResult,
        symbol: SymbolInfo,
        exception_type: Optional[str],
    ) -> None:
        """Add similar code patterns using AST-based similarity."""
        code = self._get_code(symbol)
        if not code:
            return

        similar_results = self._similarity_finder.find_similar(
            code,
            file_path=symbol.file_path,
            limit=self.max_similar,
            min_similarity=0.3,
        )
        for sim in similar_results:
            result.add(ContextItem(
                context_type=ContextType.SIMILAR_CODE,
                file_path=sim.file_path,
                start_line=sim.start_line,
                end_line=sim.end_line,
                code=sim.code,
                relevance_score=sim.similarity_score * 0.5,
                description=f"Similar to {symbol.name}: {sim.match_reason}",
                symbol_name=sim.symbol_name,
            ))

    def _add_tests(self, result: ContextResult, file_path: str) -> None:
        """Add related test files."""
        test_files = self.indexer.get_related_tests(file_path)

        for test_file in test_files[:2]:  # Max 2 test files
            symbols = self.indexer.symbol_table.find_in_file(test_file)

            # Get first test function
            for symbol in symbols:
                if symbol.name.startswith("test_"):
                    code = self._get_code(symbol)
                    if code:
                        result.add(ContextItem(
                            context_type=ContextType.TEST,
                            file_path=test_file,
                            start_line=symbol.start_line,
                            end_line=symbol.end_line,
                            code=code,
                            relevance_score=0.5,
                            description=f"Related test: {symbol.name}",
                            symbol_name=symbol.name,
                        ))
                        break

    def _truncate_if_needed(self, result: ContextResult) -> None:
        """Truncate context if it exceeds max lines."""
        if result.total_lines <= self.max_context_lines:
            return

        # Keep highest relevance items
        kept_items = []
        current_lines = 0

        for item in result.items:
            if current_lines + item.lines_count <= self.max_context_lines:
                kept_items.append(item)
                current_lines += item.lines_count
            else:
                result.truncated = True

        result.items = kept_items
        result.total_lines = current_lines

    def build_quick(
        self,
        file_path: str,
        line_number: int,
    ) -> ContextResult:
        """Build minimal context quickly.

        Only includes the failing function and its signature.
        Useful for quick lookups.

        Args:
            file_path: Path to the failing file
            line_number: Line number of failure

        Returns:
            Minimal ContextResult
        """
        result = ContextResult()

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

        # Get the failing symbol only
        symbol = self.indexer.get_symbol_at(file_path, line_number)
        if symbol:
            code = self._get_code(symbol)
            if code:
                result.add(ContextItem(
                    context_type=ContextType.FAILING_CODE,
                    file_path=symbol.file_path,
                    start_line=symbol.start_line,
                    end_line=symbol.end_line,
                    code=code,
                    relevance_score=1.0,
                    description=f"Failing function: {symbol.name}",
                    symbol_name=symbol.name,
                    metadata={
                        "signature": symbol.signature,
                        "docstring": symbol.docstring,
                    },
                ))

        return result

    def get_expanded_context(
        self,
        file_path: str,
        line_number: int,
        depth: int = 2,
    ) -> ContextResult:
        """Get expanded context with transitive callers/callees.

        Args:
            file_path: Path to the failing file
            line_number: Line number of failure
            depth: How deep to follow call chains

        Returns:
            ContextResult with expanded context
        """
        result = ContextResult()

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

        symbol = self.indexer.get_symbol_at(file_path, line_number)
        if not symbol:
            return result

        # Add failing code
        code = self._get_code(symbol)
        if code:
            result.add(ContextItem(
                context_type=ContextType.FAILING_CODE,
                file_path=symbol.file_path,
                start_line=symbol.start_line,
                end_line=symbol.end_line,
                code=code,
                relevance_score=1.0,
                description=f"Failing function: {symbol.name}",
                symbol_name=symbol.name,
            ))

        # Get transitive callers
        self._add_transitive_callers(result, symbol, depth)

        # Get transitive callees
        self._add_transitive_callees(result, symbol, depth)

        # Truncate
        self._truncate_if_needed(result)

        return result

    def _add_transitive_callers(
        self,
        result: ContextResult,
        symbol: SymbolInfo,
        depth: int,
    ) -> None:
        """Add transitive callers up to depth."""
        # Find function in call graph
        func_full_name = None
        for full_name, func_info in self.indexer.call_graph.functions.items():
            if func_info.name == symbol.name:
                func_full_name = full_name
                break

        if not func_full_name:
            return

        # Get transitive callers
        transitive = self.indexer.call_graph.get_transitive_callers(
            func_full_name,
            max_depth=depth,
        )

        for i, caller_name in enumerate(list(transitive)[:5]):
            caller_symbol = self.indexer.symbol_table.get(caller_name)
            if caller_symbol:
                code = self._get_code(caller_symbol)
                if code:
                    relevance = 0.7 - (i * 0.1)
                    result.add(ContextItem(
                        context_type=ContextType.CALLER,
                        file_path=caller_symbol.file_path,
                        start_line=caller_symbol.start_line,
                        end_line=caller_symbol.end_line,
                        code=code,
                        relevance_score=max(relevance, 0.3),
                        description=f"Transitively calls {symbol.name}",
                        symbol_name=caller_symbol.name,
                    ))

    def _add_transitive_callees(
        self,
        result: ContextResult,
        symbol: SymbolInfo,
        depth: int,
    ) -> None:
        """Add transitive callees up to depth."""
        # Find function in call graph
        func_full_name = None
        for full_name, func_info in self.indexer.call_graph.functions.items():
            if func_info.name == symbol.name:
                func_full_name = full_name
                break

        if not func_full_name:
            return

        # Get transitive callees
        transitive = self.indexer.call_graph.get_transitive_callees(
            func_full_name,
            max_depth=depth,
        )

        for i, callee_name in enumerate(list(transitive)[:5]):
            callee_symbol = self.indexer.symbol_table.get(callee_name)
            if callee_symbol:
                code = self._get_code(callee_symbol)
                if code:
                    relevance = 0.65 - (i * 0.1)
                    result.add(ContextItem(
                        context_type=ContextType.CALLEE,
                        file_path=callee_symbol.file_path,
                        start_line=callee_symbol.start_line,
                        end_line=callee_symbol.end_line,
                        code=code,
                        relevance_score=max(relevance, 0.25),
                        description=f"Transitively called by {symbol.name}",
                        symbol_name=callee_symbol.name,
                    ))
