"""Dynamic analysis for extracting runtime information from failures.

Inspects stack frames and variables at exception time to provide
concrete execution context for the LLM.
"""

from __future__ import annotations

import itertools
import sys
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple

if TYPE_CHECKING:
    import types


@dataclass
class VariableSnapshot:
    """Snapshot of a variable at a specific point in execution."""

    name: str
    value: Any
    type_name: str
    repr_value: str  # Safe string representation
    line: int
    scope: str  # "local", "global", "closure", "argument"

    def __str__(self) -> str:
        return f"{self.name}: {self.type_name} = {self.repr_value}"

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "name": self.name,
            "type": self.type_name,
            "repr": self.repr_value,
            "line": self.line,
            "scope": self.scope,
        }


@dataclass
class ExecutionFrame:
    """Information about a single stack frame."""

    function_name: str
    file_path: str
    line_number: int
    local_variables: List[VariableSnapshot] = field(default_factory=list)
    arguments: Dict[str, VariableSnapshot] = field(default_factory=dict)
    code_context: Optional[str] = None  # The actual line of code

    def __str__(self) -> str:
        return f"{self.function_name} at {self.file_path}:{self.line_number}"

    def get_variable(self, name: str) -> Optional[VariableSnapshot]:
        """Get a variable by name from locals or arguments."""
        for var in self.local_variables:
            if var.name == name:
                return var
        return self.arguments.get(name)

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "function": self.function_name,
            "file": self.file_path,
            "line": self.line_number,
            "locals": [v.to_dict() for v in self.local_variables],
            "arguments": {k: v.to_dict() for k, v in self.arguments.items()},
            "code": self.code_context,
        }


@dataclass
class DynamicContext:
    """Full dynamic context captured at exception time."""

    frames: List[ExecutionFrame] = field(default_factory=list)
    exception_frame: Optional[ExecutionFrame] = None
    variable_history: Dict[str, List[VariableSnapshot]] = field(default_factory=dict)
    inferred_types: Dict[str, str] = field(default_factory=dict)
    exception_type: str = ""
    exception_message: str = ""

    def __str__(self) -> str:
        frames_str = f"{len(self.frames)} frames"
        if self.exception_frame:
            return f"DynamicContext({frames_str}, exception at {self.exception_frame})"
        return f"DynamicContext({frames_str})"

    def get_variable_at_exception(self, name: str) -> Optional[VariableSnapshot]:
        """Get a variable's value at the exception frame."""
        if self.exception_frame:
            return self.exception_frame.get_variable(name)
        return None

    def get_all_variables(self) -> Dict[str, VariableSnapshot]:
        """Get all variables from all frames, most recent first."""
        all_vars: Dict[str, VariableSnapshot] = {}
        for frame in reversed(self.frames):
            for var in frame.local_variables:
                if var.name not in all_vars:
                    all_vars[var.name] = var
            for name, var in frame.arguments.items():
                if name not in all_vars:
                    all_vars[name] = var
        return all_vars

    def to_dict(self) -> Dict[str, Any]:
        """Convert to dictionary for serialization."""
        return {
            "exception_type": self.exception_type,
            "exception_message": self.exception_message,
            "frames": [f.to_dict() for f in self.frames],
            "inferred_types": self.inferred_types,
        }

    def format_for_prompt(self) -> str:
        """Format context for inclusion in LLM prompt."""
        lines = ["## Runtime Context"]

        if self.exception_frame:
            lines.append(f"\n### Exception at {self.exception_frame}")
            lines.append(f"- Exception: {self.exception_type}: {self.exception_message}")
            lines.append("\n### Variables at Exception:")
            for var in self.exception_frame.local_variables:
                lines.append(f"- `{var.name}`: {var.repr_value} (type: {var.type_name})")
            for name, var in self.exception_frame.arguments.items():
                lines.append(f"- `{name}` (arg): {var.repr_value} (type: {var.type_name})")

        if self.inferred_types:
            lines.append("\n### Inferred Types:")
            for name, type_name in self.inferred_types.items():
                lines.append(f"- `{name}`: {type_name}")

        return "\n".join(lines)


class DynamicInspector:
    """Extracts runtime information from failure context.

    Captures variable values, types, and execution state from
    stack frames at exception time.
    """

    MAX_VALUE_LENGTH = 500
    MAX_COLLECTION_ITEMS = 10
    MAX_STRING_LENGTH = 200
    MAX_FRAMES = 20

    def __init__(
        self,
        max_value_length: int = 500,
        max_collection_items: int = 10,
    ):
        """Initialize the dynamic inspector.

        Args:
            max_value_length: Maximum length for repr strings
            max_collection_items: Maximum items to show from collections
        """
        self.max_value_length = max_value_length
        self.max_collection_items = max_collection_items
        self._excluded_names = {"__builtins__", "__doc__", "__name__", "__package__"}

    def inspect_exception(
        self,
        exc_info: Tuple[type, BaseException, types.TracebackType],
    ) -> DynamicContext:
        """Extract dynamic context from exception info.

        Args:
            exc_info: Tuple from sys.exc_info()

        Returns:
            DynamicContext with frame and variable information
        """
        exc_type, exc_value, exc_tb = exc_info

        frames = []
        current_tb = exc_tb

        while current_tb and len(frames) < self.MAX_FRAMES:
            frame = current_tb.tb_frame
            frames.append(self._extract_frame(frame, current_tb.tb_lineno))
            current_tb = current_tb.tb_next

        exception_frame = frames[-1] if frames else None

        return DynamicContext(
            frames=frames,
            exception_frame=exception_frame,
            variable_history=self._build_variable_history(frames),
            inferred_types=self._infer_types(frames),
            exception_type=exc_type.__name__ if exc_type else "Unknown",
            exception_message=str(exc_value) if exc_value else "",
        )

    def inspect_current_exception(self) -> Optional[DynamicContext]:
        """Inspect the current exception (if any).

        Returns:
            DynamicContext or None if no current exception
        """
        exc_info = sys.exc_info()
        if exc_info[0] is None:
            return None
        return self.inspect_exception(exc_info)

    def _extract_frame(
        self,
        frame: types.FrameType,
        line_number: int,
    ) -> ExecutionFrame:
        """Extract variable information from a stack frame.

        Args:
            frame: Python frame object
            line_number: Line number in the frame

        Returns:
            ExecutionFrame with variable snapshots
        """
        local_vars = []

        # Snapshot local variables
        for name, value in frame.f_locals.items():
            if name not in self._excluded_names and not name.startswith("_"):
                local_vars.append(
                    self._snapshot_variable(name, value, line_number, "local")
                )

        # Extract function arguments
        code = frame.f_code
        arg_count = code.co_argcount
        # Include keyword-only args
        kwonly_count = code.co_kwonlyargcount
        total_args = arg_count + kwonly_count

        arg_names = code.co_varnames[:total_args]
        arguments: Dict[str, VariableSnapshot] = {}

        for name in arg_names:
            if name in frame.f_locals:
                arguments[name] = self._snapshot_variable(
                    name, frame.f_locals[name], line_number, "argument"
                )

        # Try to get the code context
        code_context = self._get_code_context(code.co_filename, line_number)

        return ExecutionFrame(
            function_name=code.co_name,
            file_path=code.co_filename,
            line_number=line_number,
            local_variables=local_vars,
            arguments=arguments,
            code_context=code_context,
        )

    def _snapshot_variable(
        self,
        name: str,
        value: Any,
        line: int,
        scope: str,
    ) -> VariableSnapshot:
        """Create a safe snapshot of a variable.

        Args:
            name: Variable name
            value: Variable value
            line: Line number
            scope: Variable scope

        Returns:
            VariableSnapshot with safe repr
        """
        type_name = type(value).__name__

        # Create safe representation
        repr_value = self._safe_repr(value)

        return VariableSnapshot(
            name=name,
            value=value,
            type_name=type_name,
            repr_value=repr_value,
            line=line,
            scope=scope,
        )

    def _safe_repr(self, value: Any) -> str:
        """Create a safe string representation of a value.

        Args:
            value: Any Python value

        Returns:
            Safe string representation
        """
        try:
            # Simple types
            if value is None:
                return "None"
            if isinstance(value, bool):
                return str(value)
            if isinstance(value, (int, float)):
                return repr(value)
            if isinstance(value, str):
                if len(value) > self.MAX_STRING_LENGTH:
                    return repr(value[: self.MAX_STRING_LENGTH]) + "..."
                return repr(value)

            # Collections
            if isinstance(value, (list, tuple)):
                type_name = type(value).__name__
                if len(value) == 0:
                    return f"{type_name}()"
                items = []
                for v in value[: self.max_collection_items]:
                    items.append(self._safe_repr_item(v))
                suffix = f"... ({len(value)} items)" if len(value) > self.max_collection_items else ""
                if type_name == "list":
                    return f"[{', '.join(items)}{suffix}]"
                return f"({', '.join(items)}{suffix})"

            if isinstance(value, dict):
                if len(value) == 0:
                    return "{}"
                items = []
                for k, v in itertools.islice(value.items(), self.max_collection_items):
                    items.append(f"{self._safe_repr_item(k)}: {self._safe_repr_item(v)}")
                suffix = f"... ({len(value)} items)" if len(value) > self.max_collection_items else ""
                return f"{{{', '.join(items)}{suffix}}}"

            if isinstance(value, set):
                if len(value) == 0:
                    return "set()"
                items = []
                for v in itertools.islice(value, self.max_collection_items):
                    items.append(self._safe_repr_item(v))
                suffix = f"... ({len(value)} items)" if len(value) > self.max_collection_items else ""
                return f"{{{', '.join(items)}{suffix}}}"

            # Objects with __dict__
            if hasattr(value, "__dict__"):
                type_name = type(value).__name__
                attrs = getattr(value, "__dict__", {})
                if not attrs:
                    return f"<{type_name}>"
                items = []
                for k, v in itertools.islice(attrs.items(), self.max_collection_items):
                    if not k.startswith("_"):
                        items.append(f"{k}={self._safe_repr_item(v)}")
                if items:
                    return f"<{type_name}: {', '.join(items)}>"
                return f"<{type_name}>"

            # Fallback
            type_name = type(value).__name__
            return f"<{type_name}>"

        except Exception:
            return f"<{type(value).__name__} - repr failed>"

    def _safe_repr_item(self, value: Any, max_len: int = 50) -> str:
        """Create a short safe repr for collection items.

        Args:
            value: Value to represent
            max_len: Maximum length

        Returns:
            Short safe representation
        """
        try:
            if value is None:
                return "None"
            if isinstance(value, bool):
                return str(value)
            if isinstance(value, (int, float)):
                return repr(value)
            if isinstance(value, str):
                if len(value) > max_len:
                    return repr(value[:max_len]) + "..."
                return repr(value)
            return f"<{type(value).__name__}>"
        except Exception:
            return "<?>"

    def _get_code_context(self, filename: str, line_number: int) -> Optional[str]:
        """Get the source code line at the given location.

        Args:
            filename: Source file path
            line_number: Line number (1-indexed)

        Returns:
            Source code line or None
        """
        try:
            import linecache

            line = linecache.getline(filename, line_number)
            return line.rstrip() if line else None
        except Exception:
            return None

    def _build_variable_history(
        self,
        frames: List[ExecutionFrame],
    ) -> Dict[str, List[VariableSnapshot]]:
        """Track how variables changed across frames.

        Args:
            frames: List of execution frames

        Returns:
            Dict mapping variable names to their history
        """
        history: Dict[str, List[VariableSnapshot]] = {}

        for frame in frames:
            for var in frame.local_variables:
                if var.name not in history:
                    history[var.name] = []
                history[var.name].append(var)

            for name, var in frame.arguments.items():
                if name not in history:
                    history[name] = []
                history[name].append(var)

        return history

    def _infer_types(
        self,
        frames: List[ExecutionFrame],
    ) -> Dict[str, str]:
        """Infer types from runtime values.

        Args:
            frames: List of execution frames

        Returns:
            Dict mapping variable names to inferred types
        """
        type_sets: Dict[str, set] = {}

        for frame in frames:
            for var in frame.local_variables:
                if var.name not in type_sets:
                    type_sets[var.name] = {var.type_name}
                else:
                    type_sets[var.name].add(var.type_name)

            for name, var in frame.arguments.items():
                if name not in type_sets:
                    type_sets[name] = {var.type_name}

        # Convert sets to type strings
        types: Dict[str, str] = {}
        for name, type_names in type_sets.items():
            if len(type_names) == 1:
                types[name] = next(iter(type_names))
            else:
                types[name] = f"Union[{', '.join(sorted(type_names))}]"

        return types


def inspect_exception(exc_info: Optional[tuple] = None) -> Optional[DynamicContext]:
    """Convenience function to inspect an exception.

    Args:
        exc_info: Exception info tuple, or None to use sys.exc_info()

    Returns:
        DynamicContext or None
    """
    if exc_info is None:
        exc_info = sys.exc_info()
    if exc_info[0] is None:
        return None

    inspector = DynamicInspector()
    return inspector.inspect_exception(exc_info)
