"""Runtime state capture for dynamic analysis.

Captures runtime variable values, execution state, and context
when exceptions occur, enabling richer fix generation.
"""

from __future__ import annotations

import sys
import time
import traceback
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional, Tuple


@dataclass
class VariableCapture:
    """Captured variable state.

    Attributes:
        name: Variable name
        type_name: Type of the variable
        value_repr: String representation of value
        value_hash: Hash for comparison
        is_none: Whether value is None
        is_empty: Whether value is empty (for collections)
        size: Size for collections
    """

    name: str
    type_name: str
    value_repr: str
    value_hash: Optional[int] = None
    is_none: bool = False
    is_empty: bool = False
    size: Optional[int] = None


@dataclass
class FrameCapture:
    """Captured stack frame state.

    Attributes:
        function_name: Name of the function
        file_path: Path to the source file
        line_number: Current line number
        local_vars: Captured local variables
        arguments: Function arguments
        code_context: Lines of code around current line
    """

    function_name: str
    file_path: str
    line_number: int
    local_vars: List[VariableCapture] = field(default_factory=list)
    arguments: Dict[str, VariableCapture] = field(default_factory=dict)
    code_context: List[str] = field(default_factory=list)


@dataclass
class ExceptionCapture:
    """Complete exception capture.

    Attributes:
        exception_type: Exception class name
        exception_message: Exception message
        frames: Captured stack frames
        timestamp: When the exception occurred
        thread_name: Name of the thread
        correlation_id: Request correlation ID if available
    """

    exception_type: str
    exception_message: str
    frames: List[FrameCapture] = field(default_factory=list)
    timestamp: float = field(default_factory=time.time)
    thread_name: str = ""
    correlation_id: Optional[str] = None
    task_id: Optional[str] = None


class RuntimeCapture:
    """Captures runtime state during execution.

    Installs hooks to capture variable values and execution state
    when exceptions occur.
    """

    # Maximum length for string representations
    MAX_REPR_LENGTH = 500

    # Maximum items to show for collections
    MAX_COLLECTION_ITEMS = 20

    # Maximum frames to capture
    MAX_FRAMES = 15

    # Variables to exclude from capture
    EXCLUDED_VARS = {"__builtins__", "__doc__", "__name__", "__package__"}

    def __init__(
        self,
        output_file: Optional[str] = None,
        capture_collections: bool = True,
        capture_code_context: bool = True,
    ):
        """Initialize runtime capture.

        Args:
            output_file: Optional file to write captures to
            capture_collections: Whether to expand collections
            capture_code_context: Whether to capture surrounding code
        """
        self.output_file = output_file
        self.capture_collections = capture_collections
        self.capture_code_context = capture_code_context

        self.captures: List[ExceptionCapture] = []
        self._original_excepthook: Optional[Callable] = None
        self._installed = False

    def install(self) -> "RuntimeCapture":
        """Install exception hook to capture runtime state.

        Returns:
            Self for chaining
        """
        if self._installed:
            return self

        self._original_excepthook = sys.excepthook
        sys.excepthook = self._exception_hook
        self._installed = True

        return self

    def uninstall(self) -> None:
        """Uninstall exception hook."""
        if self._installed and self._original_excepthook:
            sys.excepthook = self._original_excepthook
            self._installed = False

    def _exception_hook(
        self,
        exc_type: type,
        exc_value: BaseException,
        exc_tb: Any,
    ) -> None:
        """Exception hook that captures runtime state.

        Args:
            exc_type: Exception type
            exc_value: Exception value
            exc_tb: Traceback object
        """
        # Capture the exception
        capture = self.capture_exception((exc_type, exc_value, exc_tb))
        self.captures.append(capture)

        # Write to file if configured
        if self.output_file:
            self._write_capture(capture)

        # Call original hook
        if self._original_excepthook:
            self._original_excepthook(exc_type, exc_value, exc_tb)

    def capture_exception(
        self, exc_info: Tuple[type, BaseException, Any]
    ) -> ExceptionCapture:
        """Capture complete exception state.

        Args:
            exc_info: Exception info tuple (type, value, traceback)

        Returns:
            ExceptionCapture with full state
        """
        exc_type, exc_value, exc_tb = exc_info

        # Get correlation ID if available
        correlation_id = self._get_correlation_id()
        task_id = self._get_task_id()

        # Get thread name
        import threading

        thread_name = threading.current_thread().name

        capture = ExceptionCapture(
            exception_type=exc_type.__name__,
            exception_message=str(exc_value),
            timestamp=time.time(),
            thread_name=thread_name,
            correlation_id=correlation_id,
            task_id=task_id,
        )

        # Capture frames
        frames = []
        current_tb = exc_tb
        frame_count = 0

        while current_tb and frame_count < self.MAX_FRAMES:
            frame = current_tb.tb_frame
            frame_capture = self._capture_frame(frame, current_tb.tb_lineno)
            frames.append(frame_capture)
            current_tb = current_tb.tb_next
            frame_count += 1

        capture.frames = frames

        return capture

    def _capture_frame(self, frame: Any, line_number: int) -> FrameCapture:
        """Capture a single stack frame.

        Args:
            frame: Frame object
            line_number: Line number in frame

        Returns:
            FrameCapture with frame state
        """
        code = frame.f_code

        frame_capture = FrameCapture(
            function_name=code.co_name,
            file_path=code.co_filename,
            line_number=line_number,
        )

        # Capture local variables
        for name, value in frame.f_locals.items():
            if name in self.EXCLUDED_VARS:
                continue
            if name.startswith("__") and name.endswith("__"):
                continue

            var_capture = self._capture_variable(name, value)
            frame_capture.local_vars.append(var_capture)

        # Identify arguments
        arg_names = code.co_varnames[: code.co_argcount]
        for name in arg_names:
            if name in frame.f_locals:
                frame_capture.arguments[name] = self._capture_variable(
                    name, frame.f_locals[name]
                )

        # Capture code context
        if self.capture_code_context:
            frame_capture.code_context = self._get_code_context(
                code.co_filename, line_number
            )

        return frame_capture

    def _capture_variable(self, name: str, value: Any) -> VariableCapture:
        """Capture a single variable.

        Args:
            name: Variable name
            value: Variable value

        Returns:
            VariableCapture with variable state
        """
        type_name = type(value).__name__
        is_none = value is None
        is_empty = False
        size = None

        # Get safe representation
        value_repr = self._safe_repr(value)

        # Get hash if possible
        value_hash = None
        try:
            value_hash = hash(value)
        except TypeError:
            pass

        # Check emptiness and size for collections
        if hasattr(value, "__len__"):
            try:
                size = len(value)
                is_empty = size == 0
            except (TypeError, AttributeError):
                pass

        return VariableCapture(
            name=name,
            type_name=type_name,
            value_repr=value_repr,
            value_hash=value_hash,
            is_none=is_none,
            is_empty=is_empty,
            size=size,
        )

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

        Args:
            value: Value to represent

        Returns:
            Truncated string representation
        """
        try:
            if value is None:
                return "None"

            if isinstance(value, (str, int, float, bool)):
                result = repr(value)

            elif isinstance(value, (list, tuple)):
                if self.capture_collections:
                    items = value[: self.MAX_COLLECTION_ITEMS]
                    items_repr = [self._safe_repr(v) for v in items]
                    bracket = "[" if isinstance(value, list) else "("
                    end_bracket = "]" if isinstance(value, list) else ")"
                    result = f"{bracket}{', '.join(items_repr)}"
                    if len(value) > self.MAX_COLLECTION_ITEMS:
                        result += f", ... ({len(value)} total)"
                    result += end_bracket
                else:
                    result = f"{type(value).__name__}({len(value)} items)"

            elif isinstance(value, dict):
                if self.capture_collections:
                    items = list(value.items())[: self.MAX_COLLECTION_ITEMS]
                    items_repr = [
                        f"{self._safe_repr(k)}: {self._safe_repr(v)}"
                        for k, v in items
                    ]
                    result = "{" + ", ".join(items_repr)
                    if len(value) > self.MAX_COLLECTION_ITEMS:
                        result += f", ... ({len(value)} total)"
                    result += "}"
                else:
                    result = f"dict({len(value)} items)"

            elif isinstance(value, set):
                if self.capture_collections:
                    items = list(value)[: self.MAX_COLLECTION_ITEMS]
                    items_repr = [self._safe_repr(v) for v in items]
                    result = "{" + ", ".join(items_repr)
                    if len(value) > self.MAX_COLLECTION_ITEMS:
                        result += f", ... ({len(value)} total)"
                    result += "}"
                else:
                    result = f"set({len(value)} items)"

            else:
                # For objects, try to get a useful representation
                result = f"<{type(value).__name__}>"
                try:
                    # Try __repr__ but with a timeout/limit
                    obj_repr = repr(value)
                    if len(obj_repr) < 200:
                        result = obj_repr
                except Exception:
                    pass

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

        # Truncate if too long
        if len(result) > self.MAX_REPR_LENGTH:
            result = result[: self.MAX_REPR_LENGTH - 3] + "..."

        return result

    def _get_code_context(
        self, filename: str, line_number: int, context_lines: int = 3
    ) -> List[str]:
        """Get lines of code around the current line.

        Args:
            filename: Source file path
            line_number: Current line
            context_lines: Number of lines before/after

        Returns:
            List of code lines with line numbers
        """
        try:
            with open(filename, "r", encoding="utf-8") as f:
                lines = f.readlines()

            start = max(0, line_number - context_lines - 1)
            end = min(len(lines), line_number + context_lines)

            context = []
            for i in range(start, end):
                prefix = ">>>" if i == line_number - 1 else "   "
                context.append(f"{prefix} {i + 1:4d}: {lines[i].rstrip()}")

            return context

        except Exception:
            return []

    def _get_correlation_id(self) -> Optional[str]:
        """Get correlation ID if available.

        Returns:
            Correlation ID or None
        """
        try:
            from autodebug_probe.correlation import get_correlation_id

            return get_correlation_id()
        except ImportError:
            return None

    def _get_task_id(self) -> Optional[str]:
        try:
            from autodebug_probe.correlation import get_async_task_id
            return get_async_task_id()
        except ImportError:
            return None

    def _write_capture(self, capture: ExceptionCapture) -> None:
        """Write capture to output file.

        Args:
            capture: Exception capture to write
        """
        import json
        from dataclasses import asdict

        try:
            with open(self.output_file, "a", encoding="utf-8") as f:
                data = asdict(capture)
                f.write(json.dumps(data, default=str) + "\n")
        except Exception:
            pass

    def get_capture_for_exception(
        self, exception_type: str, line: Optional[int] = None
    ) -> Optional[ExceptionCapture]:
        """Get captured state for a specific exception.

        Args:
            exception_type: Exception type to find
            line: Optional line number to match

        Returns:
            ExceptionCapture if found
        """
        for capture in reversed(self.captures):
            if capture.exception_type == exception_type:
                if line is None:
                    return capture
                for frame in capture.frames:
                    if frame.line_number == line:
                        return capture
        return None

    def get_variable_at_exception(
        self, exception_type: str, variable_name: str
    ) -> Optional[VariableCapture]:
        """Get a specific variable value from an exception capture.

        Args:
            exception_type: Exception type
            variable_name: Variable name to find

        Returns:
            VariableCapture if found
        """
        capture = self.get_capture_for_exception(exception_type)
        if not capture:
            return None

        for frame in capture.frames:
            for var in frame.local_vars:
                if var.name == variable_name:
                    return var
            if variable_name in frame.arguments:
                return frame.arguments[variable_name]

        return None

    def clear(self) -> None:
        """Clear all captures."""
        self.captures.clear()

    def format_capture(self, capture: ExceptionCapture) -> str:
        """Format capture as human-readable text.

        Args:
            capture: Capture to format

        Returns:
            Formatted string
        """
        lines = [
            f"Exception: {capture.exception_type}: {capture.exception_message}",
            f"Thread: {capture.thread_name}",
        ]

        if capture.correlation_id:
            lines.append(f"Correlation ID: {capture.correlation_id}")

        lines.append("")
        lines.append("Stack Frames:")

        for i, frame in enumerate(capture.frames):
            lines.append(f"\n  Frame {i}: {frame.function_name}")
            lines.append(f"    File: {frame.file_path}:{frame.line_number}")

            if frame.arguments:
                lines.append("    Arguments:")
                for name, var in frame.arguments.items():
                    lines.append(f"      {name}: {var.value_repr}")

            if frame.local_vars:
                lines.append("    Local Variables:")
                for var in frame.local_vars[:10]:  # Limit display
                    lines.append(f"      {var.name}: {var.value_repr}")

            if frame.code_context:
                lines.append("    Code:")
                for line in frame.code_context:
                    lines.append(f"    {line}")

        return "\n".join(lines)


# Singleton instance
_default_capture: Optional[RuntimeCapture] = None


def get_runtime_capture() -> RuntimeCapture:
    """Get or create the default runtime capture instance.

    Returns:
        RuntimeCapture singleton
    """
    global _default_capture
    if _default_capture is None:
        _default_capture = RuntimeCapture()
    return _default_capture


def install_runtime_capture(**kwargs) -> RuntimeCapture:
    """Install runtime capture with given options.

    Args:
        **kwargs: Options for RuntimeCapture

    Returns:
        Installed RuntimeCapture
    """
    global _default_capture
    _default_capture = RuntimeCapture(**kwargs)
    return _default_capture.install()


__all__ = [
    "VariableCapture",
    "FrameCapture",
    "ExceptionCapture",
    "RuntimeCapture",
    "get_runtime_capture",
    "install_runtime_capture",
]
