"""Bootstrap helpers for wiring runtime dependencies.

This module centralizes object graph creation (composition root helpers) so that
the rest of the codebase can avoid module-level singletons and implicit globals.

It intentionally returns plain objects (repository, buses, security plumbing)
without starting any background threads.
"""

from __future__ import annotations

from dataclasses import dataclass
import os
from typing import Any, cast, Protocol, runtime_checkable

from ..application.event_handlers import get_security_handler
from ..application.ports.observability import NullObservabilityAdapter, ObservabilityPort
from ..domain.contracts.persistence import IAuditRepository, IMcpServerConfigRepository
from ..domain.repository import InMemoryMcpServerRepository, IMcpServerRepository
from ..domain.security.input_validator import InputValidator
from ..domain.security.rate_limiter import get_rate_limiter, InMemoryRateLimiter, RateLimitConfig
from ..infrastructure.caller_rate_limit import configure_caller_rate_limit, parse_per_caller
from ..infrastructure.command_bus import CommandBus, get_command_bus, RateLimitMiddleware
from ..infrastructure.event_bus import EventBus, get_event_bus
from ..infrastructure.persistence import (
    Database,
    DatabaseConfig,
    InMemoryAuditRepository,
    InMemoryMcpServerConfigRepository,
    RecoveryService,
    SQLiteAuditRepository,
    SQLiteMcpServerConfigRepository,
)
from ..infrastructure.query_bus import get_query_bus, QueryBus

# Default command-bus rate limit (used when neither config nor env is set).
DEFAULT_RATE_LIMIT_RPS = "10"
DEFAULT_RATE_LIMIT_BURST = "20"


def resolve_rate_limit_config(
    rate_limit: dict[str, Any] | None = None,
    env: dict[str, str] | None = None,
) -> RateLimitConfig:
    """Resolve the command-bus rate limit.

    Precedence for both ``rps`` and ``burst``: config value > env var > default.

    Args:
        rate_limit: Optional ``rate_limit`` config section
            (``{"rps": <int>, "burst": <int>}``; both keys optional).
        env: Optional environment mapping (defaults to ``os.environ``).

    Returns:
        A ``RateLimitConfig`` with the resolved ``requests_per_second`` / ``burst_size``.
    """
    env = dict(os.environ) if env is None else env
    rate_limit = rate_limit or {}

    rps = rate_limit.get("rps")
    requests_per_second = float(rps if rps is not None else env.get("MCP_RATE_LIMIT_RPS", DEFAULT_RATE_LIMIT_RPS))

    burst = rate_limit.get("burst")
    burst_size = int(burst if burst is not None else env.get("MCP_RATE_LIMIT_BURST", DEFAULT_RATE_LIMIT_BURST))

    return RateLimitConfig(
        requests_per_second=requests_per_second,
        burst_size=burst_size,
    )


# =============================================================================
# Protocol Interfaces for Runtime Dependencies
# =============================================================================


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

    def consume(self, key: str) -> Any:
        """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 event handler."""

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

    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."""
        ...

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


# The config and audit repositories are typed by the canonical contracts in
# `domain.contracts.persistence`. This module used to declare its own narrower
# Protocols for them, which is why the `RecoveryService` call below needed a
# `cast(Any, ...)`: two structural protocols for one concern, and the local pair
# was missing `delete` and `exists`. Anything that needed those -- the fleet
# writer does -- was rejected by the type checker while working perfectly at
# runtime, which is the sort of disagreement that gets resolved with a cast and
# then stops being noticed.


@dataclass(frozen=True)
class PersistenceConfig:
    """Configuration for persistence layer."""

    enabled: bool = False
    database_path: str = "data/mcp_hangar.db"
    enable_wal: bool = True
    auto_recover: bool = True


@dataclass(frozen=True)
class ObservabilityConfig:
    """Configuration for observability integrations.

    Supports Langfuse for LLM observability and tracing.

    Attributes:
        langfuse_enabled: Whether Langfuse integration is active.
        langfuse_public_key: Langfuse public API key.
        langfuse_secret_key: Langfuse secret API key.
        langfuse_host: Langfuse host URL.
        langfuse_sample_rate: Fraction of traces to sample (0.0 to 1.0).
        langfuse_scrub_inputs: Whether to redact sensitive inputs.
        langfuse_scrub_outputs: Whether to redact sensitive outputs.
    """

    langfuse_enabled: bool = False
    langfuse_public_key: str = ""
    langfuse_secret_key: str = ""
    langfuse_host: str = "https://cloud.langfuse.com"
    langfuse_sample_rate: float = 1.0
    langfuse_scrub_inputs: bool = False
    langfuse_scrub_outputs: bool = False


@dataclass(frozen=True)
class Runtime:
    """Container for runtime dependencies.

    Uses Protocol interfaces for type safety while maintaining flexibility.
    """

    repository: IMcpServerRepository
    event_bus: EventBus
    command_bus: CommandBus
    query_bus: QueryBus

    rate_limit_config: RateLimitConfig
    rate_limiter: IRateLimiter

    input_validator: InputValidator
    security_handler: ISecurityHandler

    # Persistence components (optional)
    persistence_config: PersistenceConfig | None = None
    database: Database | None = None
    config_repository: IMcpServerConfigRepository | None = None
    audit_repository: IAuditRepository | None = None
    recovery_service: RecoveryService | None = None

    # Observability components (optional)
    observability_config: ObservabilityConfig | None = None
    observability: ObservabilityPort | None = None


def apply_rate_limit_config(
    runtime: Runtime,
    full_config: dict[str, Any],
    *,
    env: dict[str, str] | None = None,
) -> RateLimitConfig:
    """Apply the config.yaml ``rate_limit`` section onto an existing runtime.

    The runtime's rate limiter is constructed from env defaults during lazy
    startup (before config.yaml is loaded). This re-resolves the effective
    config with config-over-env precedence and applies it to the already
    constructed limiter, so declarative ``config.yaml`` tuning takes effect.

    Args:
        runtime: The runtime whose rate limiter should be updated in place.
        full_config: The full configuration dictionary (may contain ``rate_limit``).
        env: Optional environment mapping (defaults to os.environ).

    Returns:
        The effective :class:`RateLimitConfig` that was applied.
    """
    rate_limit_section = full_config.get("rate_limit")
    if rate_limit_section is not None and not isinstance(rate_limit_section, dict):
        rate_limit_section = None

    effective = resolve_rate_limit_config(rate_limit_section, env=env)

    limiter = runtime.rate_limiter
    if isinstance(limiter, InMemoryRateLimiter):
        limiter.config = effective
        # Drop any buckets built with the previous config so new limits apply.
        limiter.reset_all()

    # Runtime is frozen; update the field so logging/serialization report the
    # effective config rather than the env-derived bootstrap value.
    object.__setattr__(runtime, "rate_limit_config", effective)

    return effective


def install_command_bus_rate_limit(
    runtime: Runtime,
    full_config: dict[str, Any],
    *,
    env: dict[str, str] | None = None,
) -> tuple[RateLimitConfig, RateLimitConfig | None]:
    """Put the ``rate_limit`` section in force on the command bus of *runtime* (#1471).

    ``rps`` and ``burst`` are the budget every caller shares, one per command
    type, as before. ``per_caller``, when set, gives each caller a budget of
    its own under it (``infrastructure/caller_rate_limit``). Adds the
    middleware that charges every command to them. Startup calls this, and so
    do the tests that serve the app, so both run the same wiring.

    Returns:
        The shared budget, and each caller's or ``None``.

    Raises:
        ValueError: ``per_caller`` is malformed. Nothing has been changed.
    """
    section = full_config.get("rate_limit")
    per_caller = parse_per_caller(section.get("per_caller") if isinstance(section, dict) else None)
    shared = apply_rate_limit_config(runtime, full_config, env=env)
    configure_caller_rate_limit(per_caller)
    runtime.command_bus.add_middleware(
        RateLimitMiddleware(
            rate_limiter=cast(Any, runtime.rate_limiter),
            # So a refusal by the bus's limiter reaches the security handler,
            # as a tool-level one does (#1495).
            security_handler=runtime.security_handler,
        )
    )
    return shared, per_caller


def create_runtime(
    *,
    repository: IMcpServerRepository | None = None,
    event_bus: EventBus | None = None,
    command_bus: CommandBus | None = None,
    query_bus: QueryBus | None = None,
    persistence_config: PersistenceConfig | None = None,
    observability_config: ObservabilityConfig | None = None,
    env: dict[str, str] | None = None,
    rate_limit: dict[str, Any] | None = None,
    persistence_backend: Any = None,
) -> Runtime:
    """Create runtime dependencies explicitly.

    Args:
        repository: Optional repository override (useful for tests).
        event_bus: Optional event bus override.
        command_bus: Optional command bus override.
        query_bus: Optional query bus override.
        persistence_config: Optional persistence configuration.
        observability_config: Optional observability configuration.
        env: Optional environment mapping (defaults to os.environ).
        rate_limit: Optional ``rate_limit`` config section
            (``{"rps": <int>, "burst": <int>}``). Values take precedence over the
            ``MCP_RATE_LIMIT_RPS`` / ``MCP_RATE_LIMIT_BURST`` env vars.

    Returns:
        Runtime container.
    """
    env = dict(os.environ) if env is None else env

    repo = repository or InMemoryMcpServerRepository()
    eb = event_bus or get_event_bus()
    cb = command_bus or get_command_bus()
    qb = query_bus or get_query_bus()

    # Precedence: config value > env var > default.
    rate_limit_config = resolve_rate_limit_config(rate_limit, env)
    rate_limiter = get_rate_limiter(rate_limit_config)

    input_validator = InputValidator(
        allow_absolute_paths=env.get("MCP_ALLOW_ABSOLUTE_PATHS", "false").lower() == "true",
    )

    security_handler = get_security_handler()

    # Configure persistence if enabled
    persistence_enabled = env.get("MCP_PERSISTENCE_ENABLED", "false").lower() == "true"

    if persistence_config is None and persistence_enabled:
        persistence_config = PersistenceConfig(
            enabled=True,
            database_path=env.get("MCP_DATABASE_PATH", "data/mcp_hangar.db"),
            enable_wal=env.get("MCP_DATABASE_WAL", "true").lower() == "true",
            auto_recover=env.get("MCP_AUTO_RECOVER", "true").lower() == "true",
        )

    database: Database | None = None
    config_repository: IMcpServerConfigRepository | None = None
    audit_repository: IAuditRepository | None = None
    recovery_service: RecoveryService | None = None

    if persistence_backend is not None:
        # The selected backend supplies these, like every other persisted
        # concern. No `Database` is built: that handle exists to create the
        # SQLite schema, and a backend's adapters create their own.
        config_repository = persistence_backend.config_repository()
        audit_repository = persistence_backend.audit_repository()
        if persistence_config is None:
            persistence_config = PersistenceConfig(enabled=True)
        recovery_service = RecoveryService(
            database=None,
            mcp_server_repository=repo,
            config_repository=config_repository,
            audit_repository=audit_repository,
            # The same store the bus appends to; recovery replays what it wrote.
            event_store=eb.event_store,
        )
    elif persistence_config and persistence_config.enabled:
        db_config = DatabaseConfig(
            path=persistence_config.database_path,
            enable_wal=persistence_config.enable_wal,
        )
        database = Database(db_config)
        config_repository = SQLiteMcpServerConfigRepository(database)
        audit_repository = SQLiteAuditRepository(database)
        recovery_service = RecoveryService(
            database=database,
            mcp_server_repository=repo,
            config_repository=config_repository,
            audit_repository=audit_repository,
            # The same store the bus appends to; recovery replays what it wrote.
            event_store=eb.event_store,
        )
    else:
        # Use in-memory repositories for non-persistent mode
        config_repository = InMemoryMcpServerConfigRepository()
        audit_repository = InMemoryAuditRepository()

    # Configure observability if enabled
    langfuse_enabled = env.get("HANGAR_LANGFUSE_ENABLED", "false").lower() == "true"

    if observability_config is None and langfuse_enabled:
        observability_config = ObservabilityConfig(
            langfuse_enabled=True,
            langfuse_public_key=env.get("LANGFUSE_PUBLIC_KEY", ""),
            langfuse_secret_key=env.get("LANGFUSE_SECRET_KEY", ""),
            langfuse_host=env.get("LANGFUSE_HOST", "https://cloud.langfuse.com"),
            langfuse_sample_rate=float(env.get("HANGAR_LANGFUSE_SAMPLE_RATE", "1.0")),
            langfuse_scrub_inputs=env.get("HANGAR_LANGFUSE_SCRUB_INPUTS", "false").lower() == "true",
            langfuse_scrub_outputs=env.get("HANGAR_LANGFUSE_SCRUB_OUTPUTS", "false").lower() == "true",
        )

    observability: ObservabilityPort = NullObservabilityAdapter()

    if observability_config and observability_config.langfuse_enabled:
        try:
            from ..infrastructure.observability import LangfuseConfig, LangfuseObservabilityAdapter

            langfuse_config = LangfuseConfig(
                enabled=True,
                public_key=observability_config.langfuse_public_key,
                secret_key=observability_config.langfuse_secret_key,
                host=observability_config.langfuse_host,
                sample_rate=observability_config.langfuse_sample_rate,
                scrub_inputs=observability_config.langfuse_scrub_inputs,
                scrub_outputs=observability_config.langfuse_scrub_outputs,
            )
            observability = LangfuseObservabilityAdapter(langfuse_config)
        except ImportError:
            import logging

            logging.getLogger(__name__).warning(
                "Langfuse enabled but package not installed. Install with: pip install mcp-hangar[observability]"
            )

    return Runtime(
        repository=repo,
        event_bus=eb,
        command_bus=cb,
        query_bus=qb,
        rate_limit_config=rate_limit_config,
        rate_limiter=rate_limiter,
        input_validator=input_validator,
        security_handler=security_handler,
        persistence_config=persistence_config,
        database=database,
        config_repository=config_repository,
        audit_repository=audit_repository,
        recovery_service=recovery_service,
        observability_config=observability_config,
        observability=observability,
    )
