"""
Fix Runtime - Main entry point for the fix application system.

The FixRuntime manages:
- Loading active fixes from the API
- Caching fixes locally
- Applying fixes via interceptors
- Tracking fix effectiveness
"""

from __future__ import annotations

import logging
import threading
from typing import Any, Callable, Dict, List, Optional, TypeVar

from risicare.runtime.applier import ApplyResult, FixApplier
from risicare.runtime.cache import FixCache
from risicare.runtime.config import ActiveFix, FixRuntimeConfig
from risicare.runtime.interceptors import (
    CompositeInterceptor,
    DefaultFixInterceptor,
    FixInterceptor,
    InterceptContext,
)
from risicare.runtime.loader import FixLoader

logger = logging.getLogger(__name__)

T = TypeVar("T")

# Global runtime instance
_runtime: Optional["FixRuntime"] = None
_runtime_lock = threading.Lock()


def get_runtime() -> Optional["FixRuntime"]:
    """Get the global fix runtime instance."""
    return _runtime


def init_runtime(
    config: Optional[FixRuntimeConfig] = None,
    auto_start: bool = True,
) -> "FixRuntime":
    """
    Initialize the global fix runtime.

    Args:
        config: Runtime configuration.
        auto_start: Whether to start immediately.

    Returns:
        The initialized FixRuntime.
    """
    global _runtime

    with _runtime_lock:
        if _runtime is not None:
            return _runtime

        _runtime = FixRuntime(config)
        if auto_start:
            _runtime.start()

        return _runtime


def shutdown_runtime() -> None:
    """Shutdown the global fix runtime."""
    global _runtime

    with _runtime_lock:
        if _runtime is not None:
            _runtime.stop()
            _runtime = None


class FixRuntime:
    """
    Main fix runtime class.

    Orchestrates fix loading, caching, and application.
    """

    def __init__(self, config: Optional[FixRuntimeConfig] = None) -> None:
        """
        Initialize the runtime.

        Args:
            config: Runtime configuration.
        """
        self._config = config or FixRuntimeConfig.from_env()
        self._cache = FixCache(self._config)
        self._loader = FixLoader(self._config, self._cache)
        self._applier = FixApplier(self._config, self._cache)
        self._interceptor = CompositeInterceptor([
            DefaultFixInterceptor(self._config, self._cache, self._applier)
        ])
        self._started = False
        self._lock = threading.Lock()

        # Effectiveness tracking
        self._fix_applications: Dict[str, int] = {}  # fix_id -> count
        self._fix_successes: Dict[str, int] = {}  # fix_id -> success count

    @property
    def config(self) -> FixRuntimeConfig:
        """Get the runtime configuration."""
        return self._config

    @property
    def is_enabled(self) -> bool:
        """Check if the runtime is enabled and started."""
        return self._config.enabled and self._started

    @property
    def cache(self) -> FixCache:
        """Get the fix cache."""
        return self._cache

    @property
    def loader(self) -> FixLoader:
        """Get the fix loader."""
        return self._loader

    @property
    def applier(self) -> FixApplier:
        """Get the fix applier."""
        return self._applier

    @property
    def interceptor(self) -> FixInterceptor:
        """Get the interceptor chain."""
        return self._interceptor

    def start(self) -> None:
        """
        Start the runtime.

        Loads initial fixes and starts background refresh.
        """
        with self._lock:
            if self._started:
                return

            if not self._config.enabled:
                logger.debug("Fix runtime disabled")
                return

            logger.info("Starting fix runtime...")

            # Start the loader (loads initial fixes and starts refresh)
            self._loader.start()

            self._started = True
            logger.info("Fix runtime started")

    def stop(self) -> None:
        """
        Stop the runtime.

        Stops background refresh and cleans up.
        """
        with self._lock:
            if not self._started:
                return

            logger.info("Stopping fix runtime...")

            self._loader.stop()
            self._cache.clear()

            self._started = False
            logger.info("Fix runtime stopped")

    def add_interceptor(self, interceptor: FixInterceptor) -> None:
        """
        Add an interceptor to the chain.

        Args:
            interceptor: Interceptor to add.
        """
        if isinstance(self._interceptor, CompositeInterceptor):
            self._interceptor.add(interceptor)

    def get_fix(
        self,
        error_code: str,
        session_id: Optional[str] = None,
    ) -> Optional[ActiveFix]:
        """
        Get applicable fix for an error code.

        Args:
            error_code: Error code to match.
            session_id: Optional session ID for A/B bucketing.

        Returns:
            ActiveFix if found and should be applied.
        """
        if not self.is_enabled:
            return None

        return self._applier.get_fix_for_error(error_code, session_id)

    def intercept_call(
        self,
        operation_type: str,
        operation_name: str,
        messages: Optional[List[Dict[str, Any]]] = None,
        params: Optional[Dict[str, Any]] = None,
        session_id: Optional[str] = None,
        trace_id: Optional[str] = None,
        span_id: Optional[str] = None,
        error_code: Optional[str] = None,
    ) -> tuple[Optional[List[Dict[str, Any]]], Optional[Dict[str, Any]], InterceptContext]:
        """
        Intercept a call and apply pre-call fixes.

        Args:
            operation_type: Type of operation (llm_call, tool_call).
            operation_name: Name of operation.
            messages: LLM messages.
            params: Call parameters.
            session_id: Session ID.
            trace_id: Trace ID.
            span_id: Span ID.
            error_code: Error code (if retrying).

        Returns:
            (modified_messages, modified_params, context)
        """
        context = InterceptContext(
            operation_type=operation_type,
            operation_name=operation_name,
            session_id=session_id,
            trace_id=trace_id,
            span_id=span_id,
            error_code=error_code,
        )

        if not self.is_enabled:
            return messages, params, context

        modified_messages, modified_params = self._interceptor.pre_call(
            context, messages, params
        )

        return modified_messages, modified_params, context

    def intercept_response(
        self,
        context: InterceptContext,
        response: Any,
    ) -> tuple[Any, bool]:
        """
        Intercept a response and apply post-call fixes.

        Args:
            context: Intercept context from pre-call.
            response: Call response.

        Returns:
            (modified_response, should_continue)
        """
        if not self.is_enabled:
            return response, True

        return self._interceptor.post_call(context, response)

    def intercept_error(
        self,
        context: InterceptContext,
        error: Exception,
    ) -> tuple[bool, Optional[Dict[str, Any]]]:
        """
        Intercept an error and decide on retry.

        Args:
            context: Intercept context.
            error: The exception.

        Returns:
            (should_retry, modified_params)
        """
        if not self.is_enabled:
            return False, None

        context.attempt += 1
        return self._interceptor.on_error(context, error)

    def wrap_call(
        self,
        operation: Callable[..., T],
        operation_type: str = "llm_call",
        operation_name: str = "unknown",
        session_id: Optional[str] = None,
        max_retries: int = 3,
    ) -> Callable[..., T]:
        """
        Wrap a call with fix interception.

        This is a convenience method that wraps an operation
        with automatic fix application and retry handling.

        Args:
            operation: The operation to wrap.
            operation_type: Type of operation.
            operation_name: Name of operation.
            session_id: Session ID.
            max_retries: Max retries on error.

        Returns:
            Wrapped operation.
        """

        def wrapped(*args: Any, **kwargs: Any) -> T:
            context = InterceptContext(
                operation_type=operation_type,
                operation_name=operation_name,
                session_id=session_id,
            )

            # Extract messages/params from kwargs if present
            messages = kwargs.get("messages")
            params = {k: v for k, v in kwargs.items() if k != "messages"}

            # Pre-call interception
            if self.is_enabled:
                modified_messages, modified_params = self._interceptor.pre_call(
                    context, messages, params
                )
                if modified_messages is not None:
                    kwargs["messages"] = modified_messages
                if modified_params:
                    kwargs.update(modified_params)

            # Execute with retry handling
            last_error = None
            for attempt in range(max_retries + 1):
                context.attempt = attempt + 1

                try:
                    result = operation(*args, **kwargs)

                    # Post-call interception
                    if self.is_enabled:
                        result, _ = self._interceptor.post_call(context, result)

                    # Track success
                    self._track_success(context)

                    return result

                except Exception as e:
                    last_error = e

                    # Error interception
                    if self.is_enabled:
                        should_retry, retry_params = self._interceptor.on_error(
                            context, e
                        )

                        if should_retry and attempt < max_retries:
                            if retry_params:
                                kwargs.update(retry_params)
                            continue

                    # Track failure
                    self._track_failure(context)
                    raise

            # Should not reach here, but just in case
            raise last_error  # type: ignore

        return wrapped

    def wrap_async_call(
        self,
        operation: Callable[..., Any],
        operation_type: str = "llm_call",
        operation_name: str = "unknown",
        session_id: Optional[str] = None,
        max_retries: int = 3,
    ) -> Callable[..., Any]:
        """
        Wrap an async call with fix interception.

        Args:
            operation: The async operation to wrap.
            operation_type: Type of operation.
            operation_name: Name of operation.
            session_id: Session ID.
            max_retries: Max retries on error.

        Returns:
            Wrapped async operation.
        """

        async def wrapped(*args: Any, **kwargs: Any) -> Any:
            context = InterceptContext(
                operation_type=operation_type,
                operation_name=operation_name,
                session_id=session_id,
            )

            messages = kwargs.get("messages")
            params = {k: v for k, v in kwargs.items() if k != "messages"}

            if self.is_enabled:
                modified_messages, modified_params = self._interceptor.pre_call(
                    context, messages, params
                )
                if modified_messages is not None:
                    kwargs["messages"] = modified_messages
                if modified_params:
                    kwargs.update(modified_params)

            last_error = None
            for attempt in range(max_retries + 1):
                context.attempt = attempt + 1

                try:
                    result = await operation(*args, **kwargs)

                    if self.is_enabled:
                        result, _ = self._interceptor.post_call(context, result)

                    self._track_success(context)
                    return result

                except Exception as e:
                    last_error = e

                    if self.is_enabled:
                        should_retry, retry_params = self._interceptor.on_error(
                            context, e
                        )

                        if should_retry and attempt < max_retries:
                            if retry_params:
                                kwargs.update(retry_params)
                            continue

                    self._track_failure(context)
                    raise

            raise last_error  # type: ignore

        return wrapped

    def _track_success(self, context: InterceptContext) -> None:
        """Track successful fix application."""
        if not self._config.track_effectiveness:
            return

        for result in context.applied_fixes:
            if result.applied and result.fix_id:
                self._fix_applications[result.fix_id] = (
                    self._fix_applications.get(result.fix_id, 0) + 1
                )
                self._fix_successes[result.fix_id] = (
                    self._fix_successes.get(result.fix_id, 0) + 1
                )

    def _track_failure(self, context: InterceptContext) -> None:
        """Track failed fix application."""
        if not self._config.track_effectiveness:
            return

        for result in context.applied_fixes:
            if result.applied and result.fix_id:
                self._fix_applications[result.fix_id] = (
                    self._fix_applications.get(result.fix_id, 0) + 1
                )
                # Don't increment success count

    def get_effectiveness_stats(self) -> Dict[str, Dict[str, Any]]:
        """
        Get effectiveness statistics for all fixes.

        Returns:
            Dict mapping fix_id to stats.
        """
        stats = {}
        for fix_id, applications in self._fix_applications.items():
            successes = self._fix_successes.get(fix_id, 0)
            stats[fix_id] = {
                "applications": applications,
                "successes": successes,
                "success_rate": successes / applications if applications > 0 else 0.0,
            }
        return stats

    def refresh_fixes(self) -> List[ActiveFix]:
        """
        Manually refresh fixes from the API.

        Returns:
            List of loaded fixes.
        """
        return self._loader.load_sync()

    async def refresh_fixes_async(self) -> List[ActiveFix]:
        """
        Manually refresh fixes asynchronously.

        Returns:
            List of loaded fixes.
        """
        return await self._loader.load_async()
