"""Application context for dependency injection.

Provides a clean way to access application services without global state.
Follows the Dependency Inversion Principle - high-level modules don't depend
on low-level modules, both depend on abstractions.
"""

from dataclasses import dataclass, field
from typing import Any, Optional, Protocol, cast, runtime_checkable, TYPE_CHECKING
from ..domain.contracts.event_bus import HandlerKind
from ..protocol import set_task_relay_wired

if TYPE_CHECKING:
    from ..application.commands.load_handlers import LoadMcpServerHandler, UnloadMcpServerHandler
    from ..application.discovery import DiscoveryOrchestrator
    from ..application.discovery.discovery_registry import DiscoveryRegistry
    from ..application.sagas import GroupRebalanceSaga
    from ..bootstrap.runtime import Runtime
    from ..domain.model import McpServer, McpServerGroup
    from ..domain.repository import IMcpServerRepository


# =============================================================================
# Protocol Interfaces (DIP - Dependency Inversion Principle)
# =============================================================================


@runtime_checkable
class ICommandBus(Protocol):
    """Interface for command bus."""

    def send(self, command: Any) -> Any:
        """Send a command and return result."""
        ...


@runtime_checkable
class IQueryBus(Protocol):
    """Interface for query bus."""

    def execute(self, query: Any) -> Any:
        """Execute a query and return result."""
        ...


@runtime_checkable
class IEventBus(Protocol):
    """Interface for event bus."""

    def publish(self, event: Any) -> None:
        """Publish an event."""
        ...

    def subscribe_to_all(self, handler: Any, *, kind: HandlerKind) -> None:
        """Subscribe to all events.

        `kind` is not optional here either. This Protocol is a structural
        duplicate of what `infrastructure.EventBus` provides -- a second one of
        those cost a `cast(Any, ...)` in `bootstrap/runtime.py` until it was
        removed -- so letting it describe a looser signature than the real bus
        would mean the type checker accepting a subscription the bus refuses.
        """
        ...


@runtime_checkable
class IRateLimitResult(Protocol):
    """Interface for rate limit check result."""

    @property
    def allowed(self) -> bool:
        """Whether the request is allowed."""
        ...

    @property
    def limit(self) -> int:
        """The rate limit."""
        ...


@runtime_checkable
class IRateLimiter(Protocol):
    """Interface for rate limiter."""

    def consume(self, key: str) -> IRateLimitResult:
        """Check rate limit for a key."""
        ...

    def get_stats(self) -> dict[str, Any]:
        """Get rate limiter statistics."""
        ...


@runtime_checkable
class ISecurityHandler(Protocol):
    """Interface for security handler."""

    def log_rate_limit_exceeded(
        self,
        mcp_server_id: str | None = None,
        limit: int = 0,
        window_seconds: int = 0,
        source_ip: str | None = None,
        *,
        scope: str = "",
        key_kind: str = "",
        key: str = "",
    ) -> None:
        """Log rate limit exceeded event."""
        ...

    def log_validation_failed(
        self,
        field: str,
        message: str,
        mcp_server_id: str | None = None,
        value: str | None = None,
    ) -> None:
        """Log validation failure."""
        ...

    def handle(self, event: Any) -> None:
        """Handle a security event."""
        ...


@dataclass
class ApplicationContext:
    """Dependency injection container for the application.

    Instead of using global variables, components receive this context
    which contains all dependencies they need. This makes testing easier
    and dependencies explicit.

    Attributes:
        runtime: The application runtime with all infrastructure
        groups: McpServer groups for load balancing
        discovery_orchestrator: Optional discovery service
        group_rebalance_saga: Optional saga for group rebalancing
        full_config: Full configuration dictionary loaded at startup (for config serialization)
    """

    runtime: "Runtime"
    groups: dict[str, "McpServerGroup"] = field(default_factory=dict)
    full_config: dict[str, Any] = field(default_factory=dict)
    discovery_orchestrator: Optional["DiscoveryOrchestrator"] = None
    group_rebalance_saga: Optional["GroupRebalanceSaga"] = None
    load_mcp_server_handler: Optional["LoadMcpServerHandler"] = None
    unload_mcp_server_handler: Optional["UnloadMcpServerHandler"] = None
    discovery_registry: Optional["DiscoveryRegistry"] = None
    auth_components: Any | None = None
    approval_gate: Any | None = None
    # ADR-014 task-relay serving surface (Phase 2). Populated by the server
    # factory ONLY when the relay_tasks_enabled kill-switch is on; the Phase-3
    # executor seam resolves the SAME instances the tasks/* handlers hold. All
    # remain None in the default dark configuration.
    governed_task_store: Any | None = None
    task_consent_gate: Any | None = None
    task_upstream_router: Any | None = None

    @property
    def repository(self) -> "IMcpServerRepository":
        """Get the mcp_server repository."""
        return self.runtime.repository

    @property
    def command_bus(self) -> ICommandBus:
        """Get the command bus."""
        return self.runtime.command_bus

    @property
    def query_bus(self) -> IQueryBus:
        """Get the query bus."""
        return self.runtime.query_bus

    @property
    def event_bus(self) -> IEventBus:
        """Get the event bus."""
        return self.runtime.event_bus

    @property
    def rate_limiter(self) -> IRateLimiter:
        """Get the rate limiter."""
        return self.runtime.rate_limiter

    @property
    def security_handler(self) -> ISecurityHandler:
        """Get the security handler."""
        return self.runtime.security_handler

    def get_mcp_server(self, mcp_server_id: str) -> Optional["McpServer"]:
        """Get a mcp_server by ID.

        Checks both static repository and runtime (hot-loaded) mcp_servers.
        """
        # First check static repository
        mcp_server = self.runtime.repository.get(mcp_server_id)
        if mcp_server is not None:
            return cast("McpServer", mcp_server)

        # Then check runtime (hot-loaded) mcp_servers
        from .state import get_runtime_mcp_servers

        runtime_store = get_runtime_mcp_servers()
        return cast(Optional["McpServer"], runtime_store.get_mcp_server(mcp_server_id))

    def mcp_server_exists(self, mcp_server_id: str) -> bool:
        """Check if a mcp_server exists.

        Checks both static repository and runtime (hot-loaded) mcp_servers.
        """
        # First check static repository
        if self.runtime.repository.exists(mcp_server_id):
            return True

        # Then check runtime (hot-loaded) mcp_servers
        from .state import get_runtime_mcp_servers

        runtime_store = get_runtime_mcp_servers()
        return runtime_store.exists(mcp_server_id)

    def get_group(self, group_id: str) -> Optional["McpServerGroup"]:
        """Get a group by ID."""
        return self.groups.get(group_id)

    def group_exists(self, group_id: str) -> bool:
        """Check if a group exists."""
        return group_id in self.groups

    @property
    def role_store(self) -> Any:
        """Get the role store from auth components, if available."""
        if self.auth_components is None:
            return None
        return getattr(self.auth_components, "role_store", None)

    @property
    def tap_store(self) -> Any:
        """Get the tool access policy store from auth components, if available."""
        if self.auth_components is None:
            return None
        return getattr(self.auth_components, "tap_store", None)


# Singleton context - initialized lazily or explicitly
_context: ApplicationContext | None = None


def get_context() -> ApplicationContext:
    """Get the application context.

    If context is not initialized, it will be lazily initialized
    with the default runtime. This supports both:
    - Explicit initialization via init_context() for full control
    - Lazy initialization for backward compatibility with tests

    Returns:
        ApplicationContext instance
    """
    global _context
    if _context is None:
        # Lazy initialization with default runtime
        from ..bootstrap.runtime import create_runtime

        runtime = create_runtime()
        _context = ApplicationContext(runtime=runtime)
    return _context


def init_context(runtime: "Runtime") -> ApplicationContext:
    """Initialize the application context.

    Args:
        runtime: The application runtime

    Returns:
        Initialized ApplicationContext
    """
    global _context
    _context = ApplicationContext(runtime=runtime)
    return _context


def reset_context() -> None:
    """Reset context (for testing).

    Also clears the protocol layer's relay flag. The two are set together by the
    single wiring seam, so they have to be cleared together too -- otherwise a
    test that enables the relay leaves the flag on for every test after it, and
    the capability gets claimed with no store behind it.
    """
    global _context
    _context = None
    set_task_relay_wired(False)
