"""Async service entrypoint for the RoomAgent runtime."""

from __future__ import annotations

import asyncio
import contextlib
import logging
import os
import argparse
from dataclasses import asdict, dataclass
from datetime import datetime, timezone

import uvicorn

from .a2a_server import build_a2a_application
from config.settings import HomeAssistantMCPSettings, LLMRole, Settings, load_settings
from .gateway_client import GatewayClient
from integrations.mcp_client import MCPToolClient, build_home_assistant_mcp_client
from integrations.llm_provider import LLMProviderRegistry, create_llm_provider_registry


logger = logging.getLogger(__name__)
DIRECT_EXECUTION_ERROR = (
    "Do not run room-agent/app/server.py directly. "
    "Use the project script entry instead, for example: "
    "`cd room-agent && uv run serve --config-path config/examples/room_agent.example.yaml "
    "--llm-config-path tests/fixtures/llm.yaml`."
)
_SETTINGS: Settings | None = None
_LLM_PROVIDER_REGISTRY: LLMProviderRegistry | None = None
_MCP_CLIENT: MCPToolClient | None = None
_CONFIG_PATH: str | None = None
_LLM_CONFIG_PATH: str | None = None


@dataclass(slots=True)
class MCPHealthStatus:
    enabled: bool = False
    healthy: bool = False
    server_name: str | None = None
    checked_at: str | None = None
    prompt_count: int | None = None
    error_type: str | None = None
    error: str | None = None


_MCP_HEALTH_STATUS = MCPHealthStatus()


def initialize_runtime_dependencies(
    *,
    settings: Settings,
    llm_provider_registry: LLMProviderRegistry | None = None,
    mcp_client: MCPToolClient | None = None,
    mcp_health_status: MCPHealthStatus | None = None,
) -> None:
    """Initialize process-wide runtime singletons explicitly."""
    global _SETTINGS
    global _LLM_PROVIDER_REGISTRY
    global _MCP_CLIENT
    global _MCP_HEALTH_STATUS

    _SETTINGS = settings
    _LLM_PROVIDER_REGISTRY = llm_provider_registry or create_llm_provider_registry(settings.llm)
    _MCP_CLIENT = mcp_client
    _MCP_HEALTH_STATUS = mcp_health_status or MCPHealthStatus()


def get_settings() -> Settings:
    """Return the process-wide settings singleton."""
    global _SETTINGS
    if _SETTINGS is None:
        _SETTINGS = load_settings(config_path=_CONFIG_PATH, llm_config_path=_LLM_CONFIG_PATH)
    return _SETTINGS


def get_mcp_client() -> MCPToolClient | None:
    """Return the process-wide MCP client singleton."""
    return _MCP_CLIENT


def get_mcp_health_status() -> dict[str, object | None]:
    """Return the last known Home Assistant MCP health status."""
    return asdict(_MCP_HEALTH_STATUS)


def parse_args() -> argparse.Namespace:
    """Parse command-line arguments for runtime configuration."""
    parser = argparse.ArgumentParser(description="Run RoomAgent runtime service.")
    parser.add_argument(
        "--config-path",
        dest="config_path",
        required=True,
        help="Path to room-agent main config file.",
    )
    parser.add_argument(
        "--llm-config-path",
        dest="llm_config_path",
        required=True,
        help="Path to room-agent LLM config file.",
    )
    return parser.parse_args()


def get_llm_provider_registry() -> LLMProviderRegistry:
    """Return the process-wide LLM provider registry singleton."""
    global _LLM_PROVIDER_REGISTRY
    if _LLM_PROVIDER_REGISTRY is None:
        settings = get_settings()
        _LLM_PROVIDER_REGISTRY = create_llm_provider_registry(settings.llm)
    return _LLM_PROVIDER_REGISTRY


def _describe_exception(exc: BaseException) -> tuple[str, str]:
    """Format exception details for structured health-check logging."""
    error_type = type(exc).__name__
    message = f"{error_type}: {exc}"

    if isinstance(exc, BaseExceptionGroup):
        nested_messages = [_describe_exception(item)[1] for item in exc.exceptions]
        if nested_messages:
            message = f"{message} | nested=[{'; '.join(nested_messages)}]"

    return error_type, message


async def probe_home_assistant_mcp(
    client: MCPToolClient | None,
    settings: HomeAssistantMCPSettings | None,
) -> MCPHealthStatus:
    """Probe prompt capability without interrupting room-agent startup."""
    status = MCPHealthStatus(
        enabled=bool(settings and settings.enabled),
        server_name=settings.server_name if settings else None,
        checked_at=datetime.now(timezone.utc).isoformat(),
    )
    if settings is None or not settings.enabled:
        return status
    if client is None:
        status.error = "Home Assistant MCP is enabled but client was not initialized."
        return status
    if not settings.health_check.enabled:
        status.healthy = True
        return status

    try:
        result = await client.list_prompts(settings.server_name)
        prompts = getattr(result, "prompts", None)
        status.prompt_count = len(prompts) if prompts is not None else 0
        status.healthy = True
    except Exception as exc:
        status.error_type, status.error = _describe_exception(exc)
    return status


@dataclass(slots=True)
class ServiceRuntime:
    """Coordinates the long-lived tasks hosted by the RoomAgent process."""

    host: str = os.getenv("ROOM_AGENT_HOST", "127.0.0.1")
    port: int = int(os.getenv("ROOM_AGENT_PORT", "10000"))
    business_poll_interval_seconds: float = 30.0
    gateway_client: GatewayClient | None = None

    async def run(self) -> None:
        """Run the HTTP server loop and the business loop in one event loop."""
        global _MCP_CLIENT
        global _MCP_HEALTH_STATUS

        settings = get_settings()
        llm_registry = get_llm_provider_registry()
        _MCP_CLIENT = build_home_assistant_mcp_client(settings.agent.home_assistant_mcp)
        _MCP_HEALTH_STATUS = await probe_home_assistant_mcp(
            _MCP_CLIENT,
            settings.agent.home_assistant_mcp,
        )
        gateway_client = self.gateway_client
        if settings.agent.gateway and gateway_client is None:
            gateway_client = GatewayClient()
            self.gateway_client = gateway_client
        logger.info(
            "RoomAgent settings loaded for agent=%s room=%s powerful_provider=%s low_cost_provider=%s",
            settings.agent.id,
            settings.agent.room_id,
            (
                type(llm_registry.get(LLMRole.POWERFUL)).__name__
                if llm_registry.get(LLMRole.POWERFUL)
                else "None"
            ),
            (
                type(llm_registry.get(LLMRole.LOW_COST)).__name__
                if llm_registry.get(LLMRole.LOW_COST)
                else "None"
            ),
        )
        logger.info(
            "Home Assistant MCP status enabled=%s healthy=%s server=%s prompt_count=%s error_type=%s error=%s",
            _MCP_HEALTH_STATUS.enabled,
            _MCP_HEALTH_STATUS.healthy,
            _MCP_HEALTH_STATUS.server_name,
            _MCP_HEALTH_STATUS.prompt_count,
            _MCP_HEALTH_STATUS.error_type,
            _MCP_HEALTH_STATUS.error,
        )

        if (
            settings.agent.gateway
            and settings.agent.gateway.register_on_startup
            and gateway_client is not None
        ):
            await gateway_client.register_agent(settings.agent)

        stop_event = asyncio.Event()
        tasks = [
            asyncio.create_task(self.run_a2a_http_server(stop_event), name="roomagent-a2a-http"),
            asyncio.create_task(self.run_business_loop(stop_event), name="roomagent-business-loop"),
        ]
        if settings.agent.gateway and gateway_client is not None:
            tasks.append(
                asyncio.create_task(
                    self.run_gateway_heartbeat_loop(stop_event),
                    name="roomagent-gateway-heartbeat",
                )
            )

        try:
            await asyncio.gather(*tasks)
        except asyncio.CancelledError:
            logger.info("RoomAgent runtime cancelled, stopping background tasks.")
            await self._shutdown_tasks(tasks, stop_event)
            raise
        finally:
            if any(not task.done() for task in tasks):
                await self._shutdown_tasks(tasks, stop_event)

    async def _shutdown_tasks(
        self,
        tasks: list[asyncio.Task[None]],
        stop_event: asyncio.Event,
    ) -> None:
        """Stop background tasks, preferring graceful shutdown before cancellation."""
        stop_event.set()
        _, pending = await asyncio.wait(tasks, timeout=5)
        for task in pending:
            task.cancel()
        if pending:
            await asyncio.gather(*pending, return_exceptions=True)

    async def run_a2a_http_server(self, stop_event: asyncio.Event) -> None:
        """Run the RoomAgent A2A HTTP service until shutdown is requested."""
        server = uvicorn.Server(
            uvicorn.Config(
                build_a2a_application(host=self.host, port=self.port).build(),
                host=self.host,
                port=self.port,
                log_level="info",
            )
        )
        server_task = asyncio.create_task(server.serve(), name="roomagent-uvicorn")

        logger.info("RoomAgent A2A HTTP server started on http://%s:%s", self.host, self.port)
        try:
            await stop_event.wait()
        finally:
            server.should_exit = True
            with contextlib.suppress(asyncio.CancelledError):
                await server_task
            logger.info("RoomAgent A2A HTTP server stopped.")

    async def run_business_loop(self, stop_event: asyncio.Event) -> None:
        """Periodic business loop placeholder."""
        logger.info("Business loop placeholder started.")
        while not stop_event.is_set():
            await self.tick_business_jobs()
            try:
                await asyncio.wait_for(
                    stop_event.wait(),
                    timeout=self.business_poll_interval_seconds,
                )
            except TimeoutError:
                continue
        logger.info("Business loop placeholder stopped.")

    async def run_gateway_heartbeat_loop(self, stop_event: asyncio.Event) -> None:
        """Send heartbeat requests to the backend gateway on the configured interval."""
        settings = get_settings()
        gateway = settings.agent.gateway
        if gateway is None:
            return

        logger.info(
            "Gateway heartbeat loop started for agent=%s interval=%ss",
            settings.agent.id,
            gateway.heartbeat_interval,
        )
        while not stop_event.is_set():
            if self.gateway_client is not None:
                await self.gateway_client.send_heartbeat(settings.agent)
            try:
                await asyncio.wait_for(
                    stop_event.wait(),
                    timeout=gateway.heartbeat_interval,
                )
            except TimeoutError:
                continue
        logger.info("Gateway heartbeat loop stopped.")

    async def tick_business_jobs(self) -> None:
        """Reserved hook for future scheduled business queries."""
        return None


async def main() -> None:
    """Module-level async entrypoint."""
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s %(levelname)s [%(name)s] %(message)s",
    )
    await ServiceRuntime().run()


def cli() -> None:
    """Synchronous console-script entrypoint for packaging tools."""
    global _CONFIG_PATH
    global _LLM_CONFIG_PATH

    args = parse_args()
    _CONFIG_PATH = args.config_path
    _LLM_CONFIG_PATH = args.llm_config_path
    with contextlib.suppress(KeyboardInterrupt):
        asyncio.run(main())


if __name__ == "__main__":
    raise SystemExit(DIRECT_EXECUTION_ERROR)
