"""Runtime dependency container.

Builds and wires together every long-lived object the engine needs:
HTTP clients, repository, audit log, kill switch, alert manager. The
:func:`build_runtime` factory returns a frozen :class:`RuntimeContext`
that the orchestrator and the cycle modules pass around — no global
state, no implicit singletons.
"""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from datetime import UTC, datetime
from pathlib import Path

import httpx

from cerbero_bite.clients._base import DEFAULT_BOT_TAG, HttpToolClient
from cerbero_bite.clients.deribit import DeribitClient
from cerbero_bite.clients.hyperliquid import HyperliquidClient
from cerbero_bite.clients.macro import MacroClient
from cerbero_bite.clients.portfolio import PortfolioClient
from cerbero_bite.clients.sentiment import SentimentClient
from cerbero_bite.clients.telegram import TelegramClient, load_telegram_credentials
from cerbero_bite.config.mcp_endpoints import McpEndpoints
from cerbero_bite.config.schema import StrategyConfig
from cerbero_bite.runtime.alert_manager import AlertManager
from cerbero_bite.safety.audit_log import AuditLog
from cerbero_bite.safety.kill_switch import KillSwitch
from cerbero_bite.state import (
    Repository,
    connect,
    run_migrations,
    transaction,
)

__all__ = ["RuntimeContext", "build_runtime"]


@dataclass(frozen=True)
class RuntimeContext:
    """Bag of every wired object used by the runtime."""

    cfg: StrategyConfig
    db_path: Path
    audit_path: Path

    repository: Repository
    audit_log: AuditLog
    kill_switch: KillSwitch
    alert_manager: AlertManager

    deribit: DeribitClient
    macro: MacroClient
    sentiment: SentimentClient
    hyperliquid: HyperliquidClient
    portfolio: PortfolioClient
    telegram: TelegramClient

    http_client: httpx.AsyncClient

    clock: Callable[[], datetime]

    async def aclose(self) -> None:
        """Close the shared HTTP client. Idempotent."""
        await self.http_client.aclose()


def _utc_now() -> datetime:
    return datetime.now(UTC)


def build_runtime(
    *,
    cfg: StrategyConfig,
    endpoints: McpEndpoints,
    token: str,
    db_path: Path | str,
    audit_path: Path | str,
    bot_tag: str = DEFAULT_BOT_TAG,
    timeout_s: float = 8.0,
    retry_max: int = 3,
    clock: Callable[[], datetime] | None = None,
) -> RuntimeContext:
    """Wire every dependency the runtime needs.

    The SQLite database is migrated and the system_state singleton is
    initialised eagerly so the orchestrator can assume both are
    present.
    """
    db_path = Path(db_path)
    audit_path = Path(audit_path)
    clk = clock or _utc_now

    repository = Repository()
    conn = connect(db_path)
    try:
        run_migrations(conn)
        with transaction(conn):
            repository.init_system_state(
                conn, config_version=cfg.config_version, now=clk()
            )
    finally:
        conn.close()

    def _persist_audit_anchor(line_hash: str) -> None:
        """Mirror the latest audit chain hash into ``system_state``.

        Best-effort: if SQLite is locked by another writer the audit
        log itself is still consistent, the anchor will catch up on
        the next append.
        """
        anchor_conn = connect(db_path)
        try:
            with transaction(anchor_conn):
                repository.set_last_audit_hash(anchor_conn, hex_hash=line_hash)
        except Exception:  # pragma: no cover — durability is best-effort
            pass
        finally:
            anchor_conn.close()

    audit_log = AuditLog(audit_path, on_append=_persist_audit_anchor)
    kill_switch = KillSwitch(
        connection_factory=lambda: connect(db_path),
        repository=repository,
        audit_log=audit_log,
        clock=clk,
    )

    # Single long-lived AsyncClient shared by every wrapper. httpx pools
    # connections per host so the snapshot stage of the entry cycle
    # avoids paying TLS/TCP handshakes on each call.
    http_client = httpx.AsyncClient(
        timeout=httpx.Timeout(timeout_s),
        limits=httpx.Limits(max_connections=20, max_keepalive_connections=10),
    )

    def _client(service: str) -> HttpToolClient:
        return HttpToolClient(
            service=service,
            base_url=endpoints.for_service(service),
            token=token,
            bot_tag=bot_tag,
            timeout_s=timeout_s,
            retry_max=retry_max,
            client=http_client,
        )

    bot_token, chat_id = load_telegram_credentials()
    telegram = TelegramClient(
        bot_token=bot_token,
        chat_id=chat_id,
        http_client=http_client,
        timeout_s=timeout_s,
    )
    alert_manager = AlertManager(
        telegram=telegram, audit_log=audit_log, kill_switch=kill_switch
    )

    deribit = DeribitClient(_client("deribit"), _client("unified"))
    macro = MacroClient(_client("macro"))
    sentiment = SentimentClient(_client("sentiment"))
    hyperliquid = HyperliquidClient(_client("hyperliquid"))
    portfolio = PortfolioClient(
        deribit=deribit, hyperliquid=hyperliquid, macro=macro
    )

    return RuntimeContext(
        cfg=cfg,
        db_path=db_path,
        audit_path=audit_path,
        repository=repository,
        audit_log=audit_log,
        kill_switch=kill_switch,
        alert_manager=alert_manager,
        deribit=deribit,
        macro=macro,
        sentiment=sentiment,
        hyperliquid=hyperliquid,
        portfolio=portfolio,
        telegram=telegram,
        http_client=http_client,
        clock=clk,
    )
