"""Runtime probe for capturing exception context during execution.

Provides hooks to capture runtime information when exceptions occur,
storing context for later analysis.
"""

from __future__ import annotations

import sys
import time
from collections import deque
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Callable, Deque, Dict, List, Optional

if TYPE_CHECKING:
    import types

from autodebug.analysis.dynamic_inspector import (
    DynamicContext,
    DynamicInspector,
)


@dataclass
class CapturedContext:
    """A captured exception context with metadata."""

    context: DynamicContext
    timestamp: float
    test_name: Optional[str] = None
    additional_data: Dict[str, Any] = field(default_factory=dict)

    def __str__(self) -> str:
        return f"CapturedContext({self.context.exception_type} at {self.timestamp})"


class RuntimeProbe:
    """Captures runtime information during test execution.

    Installs hooks to automatically capture exception context,
    storing information for later retrieval and analysis.
    """

    def __init__(
        self,
        inspector: Optional[DynamicInspector] = None,
        max_captures: int = 100,
    ):
        """Initialize the runtime probe.

        Args:
            inspector: DynamicInspector to use, or None for default
            max_captures: Maximum captures to store
        """
        self.inspector = inspector or DynamicInspector()
        self.max_captures = max_captures
        self.captures: Deque[CapturedContext] = deque(maxlen=max_captures)
        self._original_excepthook: Optional[Callable] = None
        self._installed = False
        self._current_test: Optional[str] = None

    def install_hook(self) -> None:
        """Install sys.excepthook to capture runtime info."""
        if self._installed:
            return

        self._original_excepthook = sys.excepthook

        def hook(
            exc_type: type,
            exc_value: BaseException,
            exc_tb: types.TracebackType,
        ) -> None:
            self.capture_on_exception((exc_type, exc_value, exc_tb))
            if self._original_excepthook:
                self._original_excepthook(exc_type, exc_value, exc_tb)

        sys.excepthook = hook
        self._installed = True

    def uninstall_hook(self) -> None:
        """Uninstall the excepthook and restore original."""
        if not self._installed:
            return

        if self._original_excepthook:
            sys.excepthook = self._original_excepthook
        self._original_excepthook = None
        self._installed = False

    def capture_on_exception(
        self,
        exc_info: tuple,
        additional_data: Optional[Dict[str, Any]] = None,
    ) -> CapturedContext:
        """Capture context when an exception occurs.

        Args:
            exc_info: Tuple from sys.exc_info()
            additional_data: Optional extra data to store

        Returns:
            The captured context
        """
        context = self.inspector.inspect_exception(exc_info)

        captured = CapturedContext(
            context=context,
            timestamp=time.time(),
            test_name=self._current_test,
            additional_data=additional_data or {},
        )

        self.captures.append(captured)

        return captured

    def capture_current_exception(
        self,
        additional_data: Optional[Dict[str, Any]] = None,
    ) -> Optional[CapturedContext]:
        """Capture context for the current exception.

        Args:
            additional_data: Optional extra data to store

        Returns:
            CapturedContext or None if no current exception
        """
        exc_info = sys.exc_info()
        if exc_info[0] is None:
            return None
        return self.capture_on_exception(exc_info, additional_data)

    def set_current_test(self, test_name: Optional[str]) -> None:
        """Set the current test name for captures.

        Args:
            test_name: Name of the current test, or None
        """
        self._current_test = test_name

    def get_context_for_exception(
        self,
        exc_type: str,
        line: Optional[int] = None,
    ) -> Optional[DynamicContext]:
        """Get captured context for a specific exception.

        Args:
            exc_type: Exception type name
            line: Optional line number filter

        Returns:
            Matching DynamicContext or None
        """
        for captured in reversed(self.captures):
            ctx = captured.context
            if ctx.exception_type == exc_type:
                if line is None:
                    return ctx
                if ctx.exception_frame and ctx.exception_frame.line_number == line:
                    return ctx
        return None

    def get_context_for_test(self, test_name: str) -> List[DynamicContext]:
        """Get all captured contexts for a specific test.

        Args:
            test_name: Name of the test

        Returns:
            List of DynamicContexts
        """
        return [
            c.context for c in self.captures if c.test_name == test_name
        ]

    def get_latest_context(self) -> Optional[DynamicContext]:
        """Get the most recent captured context.

        Returns:
            Most recent DynamicContext or None
        """
        return self.captures[-1].context if self.captures else None

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

    def __enter__(self) -> RuntimeProbe:
        """Context manager entry - install hook."""
        self.install_hook()
        return self

    def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
        """Context manager exit - uninstall hook."""
        self.uninstall_hook()


class ProbeContextManager:
    """Context manager for capturing exceptions in a code block."""

    def __init__(
        self,
        probe: Optional[RuntimeProbe] = None,
        test_name: Optional[str] = None,
    ):
        """Initialize the context manager.

        Args:
            probe: RuntimeProbe to use, or None for a new one
            test_name: Name to associate with captures
        """
        self.probe = probe or RuntimeProbe()
        self.test_name = test_name
        self.captured: Optional[CapturedContext] = None

    def __enter__(self) -> ProbeContextManager:
        """Enter the context - set up for capture."""
        self.probe.set_current_test(self.test_name)
        return self

    def __exit__(
        self,
        exc_type: Optional[type],
        exc_val: Optional[BaseException],
        exc_tb: Any,
    ) -> bool:
        """Exit the context - capture exception if one occurred."""
        if exc_type is not None:
            self.captured = self.probe.capture_current_exception()
        self.probe.set_current_test(None)
        return False  # Don't suppress the exception


def capture_exception_context(func: Callable) -> Callable:
    """Decorator to capture exception context from a function.

    Usage:
        @capture_exception_context
        def my_function():
            ...

    The captured context is available as func.__captured_context__
    """
    probe = RuntimeProbe()

    def wrapper(*args: Any, **kwargs: Any) -> Any:
        try:
            return func(*args, **kwargs)
        except Exception:
            wrapper.__captured_context__ = probe.capture_current_exception()
            raise

    wrapper.__captured_context__ = None
    return wrapper


# Global probe instance for convenience
_global_probe: Optional[RuntimeProbe] = None


def get_global_probe() -> RuntimeProbe:
    """Get or create the global runtime probe.

    Returns:
        Global RuntimeProbe instance
    """
    global _global_probe
    if _global_probe is None:
        _global_probe = RuntimeProbe()
    return _global_probe


def install_global_hook() -> None:
    """Install the global exception capture hook."""
    get_global_probe().install_hook()


def uninstall_global_hook() -> None:
    """Uninstall the global exception capture hook."""
    if _global_probe:
        _global_probe.uninstall_hook()


def get_latest_context() -> Optional[DynamicContext]:
    """Get the latest captured context from the global probe.

    Returns:
        Most recent DynamicContext or None
    """
    if _global_probe:
        return _global_probe.get_latest_context()
    return None
